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:
parent
69420ab9e4
commit
65876fb895
4 changed files with 134 additions and 29 deletions
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(¶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::<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};
|
||||||
|
|
|
||||||
|
|
@ -109,16 +109,34 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="tab-futures" style="display:none">
|
<div id="tab-futures" style="display:none">
|
||||||
<section>
|
<div style="display:flex;gap:6px;margin-bottom:12px">
|
||||||
<div class="section-header">
|
<button class="tab active" id="ftab-btn-usdm" onclick="showFuturesTab('usdm')">USD-M</button>
|
||||||
<span class="section-title">USD-M Futures</span>
|
<button class="tab" id="ftab-btn-coinm" onclick="showFuturesTab('coinm')">Coin-M</button>
|
||||||
<span class="total-badge" id="futures-total">—</span>
|
</div>
|
||||||
</div>
|
<div id="ftab-usdm">
|
||||||
<table>
|
<section>
|
||||||
<thead><tr><th>Varlık</th><th>Cüzdan Bakiyesi</th><th>Gerçekleşmemiş PnL</th><th>Kullanılabilir</th></tr></thead>
|
<div class="section-header">
|
||||||
<tbody id="futures-body"><tr><td colspan="4" class="loading">Yükleniyor...</td></tr></tbody>
|
<span class="section-title">USD-M Futures</span>
|
||||||
</table>
|
<span class="total-badge" id="futures-usdm-total">—</span>
|
||||||
</section>
|
</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-usdm-body"><tr><td colspan="4" class="loading">Yükleniyor...</td></tr></tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</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>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
@ -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 = '/';
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue