feat(mse): earn/futures wallet tabs + 8-decimal price formatting
- BinanceClient::get_earn_flexible_balances() — /sapi/v1/simple-earn/flexible/position - BinanceClient::get_futures_balances() — /fapi/v2/account (fapi.binance.com) - GET /api/wallet/earn and /api/wallet/futures endpoints - wallet.html: Earn and Futures tabs now show real data - fmt() default changed from d=2 to d=8 (trailing zeros stripped) across bot.html, positions.html, index.html — fixes DOGE showing as 0.10 instead of 0.09849 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
799cd53695
commit
bad653ff2d
7 changed files with 288 additions and 78 deletions
|
|
@ -24,6 +24,8 @@ pub fn build(state: AppState) -> Router {
|
|||
.route("/bots/:id/positions/:order_id/cancel", post(bots::cancel_position))
|
||||
.route("/bots/:id/positions/:order_id/sell", post(bots::sell_position))
|
||||
.route("/wallet/spot", get(wallet::spot_balances))
|
||||
.route("/wallet/earn", get(wallet::earn_balances))
|
||||
.route("/wallet/futures", get(wallet::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))
|
||||
|
|
|
|||
|
|
@ -5,26 +5,77 @@ use crate::binance::client::BinanceClient;
|
|||
use crate::AppState;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct BalanceEntry {
|
||||
pub struct SpotBalance {
|
||||
pub asset: String,
|
||||
pub free: f64,
|
||||
pub locked: f64,
|
||||
}
|
||||
|
||||
pub async fn spot_balances(State(state): State<AppState>) -> impl IntoResponse {
|
||||
let mode = state.current_mode.lock().await.clone();
|
||||
#[derive(Serialize)]
|
||||
pub struct EarnBalance {
|
||||
pub asset: String,
|
||||
pub total: f64,
|
||||
pub free: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct FuturesBalance {
|
||||
pub asset: String,
|
||||
pub wallet: f64,
|
||||
pub unrealized_pnl: f64,
|
||||
pub available: f64,
|
||||
}
|
||||
|
||||
fn make_client(state: &AppState, mode: &str) -> BinanceClient {
|
||||
let (api_key, api_secret) = if mode == "live" {
|
||||
(state.live_api_key.clone(), state.live_api_secret.clone())
|
||||
} else {
|
||||
(state.testnet_api_key.clone(), state.testnet_api_secret.clone())
|
||||
};
|
||||
let client = BinanceClient::new(api_key, api_secret, mode == "testnet");
|
||||
BinanceClient::new(api_key, api_secret, mode == "testnet")
|
||||
}
|
||||
|
||||
pub async fn spot_balances(State(state): State<AppState>) -> impl IntoResponse {
|
||||
let mode = state.current_mode.lock().await.clone();
|
||||
let client = make_client(&state, &mode);
|
||||
|
||||
match client.get_account_balances().await {
|
||||
Ok(balances) => {
|
||||
let entries: Vec<BalanceEntry> = balances
|
||||
let entries: Vec<SpotBalance> = balances
|
||||
.into_iter()
|
||||
.map(|(asset, free, locked)| BalanceEntry { asset, free, locked })
|
||||
.map(|(asset, free, locked)| SpotBalance { asset, free, locked })
|
||||
.collect();
|
||||
Json(entries).into_response()
|
||||
}
|
||||
Err(e) => (axum::http::StatusCode::BAD_GATEWAY, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn earn_balances(State(state): State<AppState>) -> impl IntoResponse {
|
||||
let mode = state.current_mode.lock().await.clone();
|
||||
let client = make_client(&state, &mode);
|
||||
|
||||
match client.get_earn_flexible_balances().await {
|
||||
Ok(balances) => {
|
||||
let entries: Vec<EarnBalance> = balances
|
||||
.into_iter()
|
||||
.map(|(asset, total, free)| EarnBalance { asset, total, free })
|
||||
.collect();
|
||||
Json(entries).into_response()
|
||||
}
|
||||
Err(e) => (axum::http::StatusCode::BAD_GATEWAY, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn 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_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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -362,6 +362,102 @@ impl BinanceClient {
|
|||
Ok(result)
|
||||
}
|
||||
|
||||
/// Simple Earn Flexible pozisyonları: Vec<(asset, total, free)>
|
||||
pub async fn get_earn_flexible_balances(&self) -> Result<Vec<(String, f64, f64)>> {
|
||||
// SAPI endpoint — testnet desteklemiyor, her zaman prod'dan çek
|
||||
let url = "https://api.binance.com/sapi/v1/simple-earn/flexible/position";
|
||||
let mut results = Vec::new();
|
||||
let mut current = 1u32;
|
||||
let size = 100u32;
|
||||
|
||||
loop {
|
||||
let timestamp = chrono::Utc::now().timestamp_millis().to_string();
|
||||
let params = format!("current={}&size={}×tamp={}", current, size, timestamp);
|
||||
let signature = self.sign(¶ms);
|
||||
|
||||
let response = self
|
||||
.http
|
||||
.get(url)
|
||||
.header("X-MBX-APIKEY", &self.api_key)
|
||||
.query(&[
|
||||
("current", current.to_string().as_str()),
|
||||
("size", size.to_string().as_str()),
|
||||
("timestamp", timestamp.as_str()),
|
||||
("signature", signature.as_str()),
|
||||
])
|
||||
.send()
|
||||
.await?
|
||||
.json::<Value>()
|
||||
.await?;
|
||||
|
||||
if let Some(code) = response.get("code") {
|
||||
return Err(anyhow!("Earn API hatası {}: {}", code, response["msg"].as_str().unwrap_or("")));
|
||||
}
|
||||
|
||||
let rows = match response["rows"].as_array() {
|
||||
Some(r) => r.clone(),
|
||||
None => break,
|
||||
};
|
||||
let total_pages = response["total"].as_u64().unwrap_or(0);
|
||||
|
||||
for row in &rows {
|
||||
let asset = match row["asset"].as_str() { Some(a) => a.to_string(), None => continue };
|
||||
let total: f64 = row["totalAmount"].as_str().unwrap_or("0").parse().unwrap_or(0.0);
|
||||
let free: f64 = row["freeAmount"].as_str().unwrap_or("0").parse().unwrap_or(0.0);
|
||||
if total > 0.0 {
|
||||
results.push((asset, total, free));
|
||||
}
|
||||
}
|
||||
|
||||
if (current as u64) * (size as u64) >= total_pages { break; }
|
||||
current += 1;
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// USD-M Futures hesap bakiyesi: Vec<(asset, wallet_balance, unrealized_pnl, available)>
|
||||
pub async fn get_futures_balances(&self) -> Result<Vec<(String, f64, f64, f64)>> {
|
||||
let futures_url = if self.base_url == BINANCE_TESTNET_URL {
|
||||
"https://testnet.binancefutures.com"
|
||||
} else {
|
||||
"https://fapi.binance.com"
|
||||
};
|
||||
let url = format!("{}/fapi/v2/account", futures_url);
|
||||
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!("Futures 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};
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ async function api(method, path, body) {
|
|||
return res;
|
||||
}
|
||||
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
function fmt(n, d=2) { return parseFloat(n).toLocaleString('tr-TR', { minimumFractionDigits: d, maximumFractionDigits: d }); }
|
||||
function fmt(n, d=8) { const v = parseFloat(n); if (isNaN(v)) return '—'; return v.toLocaleString('tr-TR', { minimumFractionDigits: 0, maximumFractionDigits: d }); }
|
||||
function shortDate(ms) { return new Date(ms).toLocaleString('tr-TR',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}); }
|
||||
function buyTime(ms) {
|
||||
const tf = TF_SECONDS[bot.timeframe] || 300;
|
||||
|
|
@ -441,12 +441,12 @@ async function loadTicker() {
|
|||
const res = await fetch(`https://api.binance.com/api/v3/ticker/24hr?symbol=${bot.symbol}`);
|
||||
const t = await res.json();
|
||||
const change = parseFloat(t.priceChangePercent);
|
||||
document.getElementById('pb-price').textContent = fmt(t.lastPrice, 2);
|
||||
document.getElementById('pb-price').textContent = fmt(t.lastPrice);
|
||||
const chEl = document.getElementById('pb-change');
|
||||
chEl.textContent = (change >= 0 ? '+' : '') + change.toFixed(2) + '%';
|
||||
chEl.style.color = change >= 0 ? 'var(--green)' : 'var(--red)';
|
||||
document.getElementById('pb-high').textContent = fmt(t.highPrice, 2);
|
||||
document.getElementById('pb-low').textContent = fmt(t.lowPrice, 2);
|
||||
document.getElementById('pb-high').textContent = fmt(t.highPrice);
|
||||
document.getElementById('pb-low').textContent = fmt(t.lowPrice);
|
||||
const vol = parseFloat(t.quoteVolume);
|
||||
document.getElementById('pb-vol').textContent = vol >= 1e9 ? (vol/1e9).toFixed(2)+'B' : vol >= 1e6 ? (vol/1e6).toFixed(2)+'M' : fmt(vol, 0);
|
||||
} catch(e) {}
|
||||
|
|
@ -461,7 +461,7 @@ function startPriceWs() {
|
|||
const price = parseFloat(t.c);
|
||||
currentPrice = price;
|
||||
const change = parseFloat(t.P);
|
||||
document.getElementById('pb-price').textContent = fmt(price, 2);
|
||||
document.getElementById('pb-price').textContent = fmt(price);
|
||||
const chEl = document.getElementById('pb-change');
|
||||
chEl.textContent = (change >= 0 ? '+' : '') + change.toFixed(2) + '%';
|
||||
chEl.style.color = change >= 0 ? 'var(--green)' : 'var(--red)';
|
||||
|
|
@ -493,10 +493,10 @@ function initChart() {
|
|||
if (!p.seriesData || !p.seriesData.size) return;
|
||||
const d = p.seriesData.get(candleSeries);
|
||||
if (!d) return;
|
||||
document.getElementById('ohlc-o').textContent = fmt(d.open, 2);
|
||||
document.getElementById('ohlc-h').textContent = fmt(d.high, 2);
|
||||
document.getElementById('ohlc-l').textContent = fmt(d.low, 2);
|
||||
document.getElementById('ohlc-c').textContent = fmt(d.close, 2);
|
||||
document.getElementById('ohlc-o').textContent = fmt(d.open);
|
||||
document.getElementById('ohlc-h').textContent = fmt(d.high);
|
||||
document.getElementById('ohlc-l').textContent = fmt(d.low);
|
||||
document.getElementById('ohlc-c').textContent = fmt(d.close);
|
||||
});
|
||||
new ResizeObserver(() => {
|
||||
chart.applyOptions({ width: container.clientWidth, height: container.clientHeight });
|
||||
|
|
@ -516,10 +516,10 @@ async function loadKlines() {
|
|||
chart.timeScale().setVisibleLogicalRange({ from: candles.length - 80, to: candles.length + 5 });
|
||||
if (candles.length) {
|
||||
const last = candles[candles.length - 1];
|
||||
document.getElementById('ohlc-o').textContent = fmt(last.open, 2);
|
||||
document.getElementById('ohlc-h').textContent = fmt(last.high, 2);
|
||||
document.getElementById('ohlc-l').textContent = fmt(last.low, 2);
|
||||
document.getElementById('ohlc-c').textContent = fmt(last.close, 2);
|
||||
document.getElementById('ohlc-o').textContent = fmt(last.open);
|
||||
document.getElementById('ohlc-h').textContent = fmt(last.high);
|
||||
document.getElementById('ohlc-l').textContent = fmt(last.low);
|
||||
document.getElementById('ohlc-c').textContent = fmt(last.close);
|
||||
}
|
||||
updateChartOverlays();
|
||||
} catch(e) {}
|
||||
|
|
@ -535,11 +535,11 @@ function startKlineWs() {
|
|||
const k = JSON.parse(e.data).k;
|
||||
const candle = { time: Math.floor(k.t/1000), open: parseFloat(k.o), high: parseFloat(k.h), low: parseFloat(k.l), close: parseFloat(k.c) };
|
||||
candleSeries.update(candle);
|
||||
document.getElementById('ohlc-o').textContent = fmt(candle.open, 2);
|
||||
document.getElementById('ohlc-h').textContent = fmt(candle.high, 2);
|
||||
document.getElementById('ohlc-l').textContent = fmt(candle.low, 2);
|
||||
document.getElementById('ohlc-c').textContent = fmt(candle.close, 2);
|
||||
document.getElementById('pb-price').textContent = fmt(candle.close, 2);
|
||||
document.getElementById('ohlc-o').textContent = fmt(candle.open);
|
||||
document.getElementById('ohlc-h').textContent = fmt(candle.high);
|
||||
document.getElementById('ohlc-l').textContent = fmt(candle.low);
|
||||
document.getElementById('ohlc-c').textContent = fmt(candle.close);
|
||||
document.getElementById('pb-price').textContent = fmt(candle.close);
|
||||
};
|
||||
klineWs.onerror = () => {};
|
||||
klineWs.onclose = () => setTimeout(startKlineWs, 5000);
|
||||
|
|
@ -553,7 +553,7 @@ function updateChartOverlays() {
|
|||
priceLines.push(candleSeries.createPriceLine({
|
||||
price: p.sell_target, color: '#2ecc71', lineWidth: 1,
|
||||
lineStyle: LightweightCharts.LineStyle.Dashed,
|
||||
axisLabelVisible: true, title: fmt(p.sell_target, 2),
|
||||
axisLabelVisible: true, title: fmt(p.sell_target),
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
@ -570,19 +570,19 @@ function updateChartOverlays() {
|
|||
const t = buyTime(p.opened_at);
|
||||
if (t < minTime || t > maxTime) return;
|
||||
if (!buyMap.has(t)) buyMap.set(t, []);
|
||||
buyMap.get(t).push(fmt(p.buy_price, 2));
|
||||
buyMap.get(t).push(fmt(p.buy_price));
|
||||
});
|
||||
closedPositionsCache.forEach(c => {
|
||||
const tb = buyTime(c.opened_at);
|
||||
if (tb >= minTime && tb <= maxTime) {
|
||||
if (!buyMap.has(tb)) buyMap.set(tb, []);
|
||||
buyMap.get(tb).push(fmt(c.buy_price, 2));
|
||||
buyMap.get(tb).push(fmt(c.buy_price));
|
||||
}
|
||||
if (c.status === 'FILLED') {
|
||||
const ts = Math.floor(c.closed_at / 1000);
|
||||
if (ts >= minTime && ts <= maxTime) {
|
||||
if (!sellMap.has(ts)) sellMap.set(ts, []);
|
||||
sellMap.get(ts).push(fmt(c.sell_target, 2));
|
||||
sellMap.get(ts).push(fmt(c.sell_target));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -217,9 +217,9 @@ function renderPositions() {
|
|||
tbody.innerHTML = page.map(p => `<tr>
|
||||
<td>${esc(p.bot_name)}</td>
|
||||
<td><strong>${esc(p.symbol)}</strong></td>
|
||||
<td>${p.buy_price.toFixed(6)}</td>
|
||||
<td>${p.sell_target.toFixed(6)}</td>
|
||||
<td>${p.quantity}</td>
|
||||
<td>${p.buy_price.toLocaleString('tr-TR',{minimumFractionDigits:0,maximumFractionDigits:8})}</td>
|
||||
<td>${p.sell_target.toLocaleString('tr-TR',{minimumFractionDigits:0,maximumFractionDigits:8})}</td>
|
||||
<td>${parseFloat(p.quantity).toLocaleString('tr-TR',{minimumFractionDigits:0,maximumFractionDigits:8})}</td>
|
||||
<td>${new Date(p.opened_at).toLocaleString('tr-TR')}</td>
|
||||
</tr>`).join('');
|
||||
renderPagination('positions-pagination', positionsCache.length, posPage, 'goPosPage');
|
||||
|
|
@ -243,9 +243,9 @@ function renderClosed() {
|
|||
tbody.innerHTML = page.map(c => `<tr>
|
||||
<td>${esc(c.bot_name)}</td>
|
||||
<td><strong>${esc(c.symbol)}</strong></td>
|
||||
<td>${c.buy_price.toFixed(6)}</td>
|
||||
<td>${c.sell_target.toFixed(6)}</td>
|
||||
<td>${c.quantity}</td>
|
||||
<td>${c.buy_price.toLocaleString('tr-TR',{minimumFractionDigits:0,maximumFractionDigits:8})}</td>
|
||||
<td>${c.sell_target.toLocaleString('tr-TR',{minimumFractionDigits:0,maximumFractionDigits:8})}</td>
|
||||
<td>${parseFloat(c.quantity).toLocaleString('tr-TR',{minimumFractionDigits:0,maximumFractionDigits:8})}</td>
|
||||
<td><span class="badge ${c.status==='FILLED'?'badge-running':'badge-stopped'}">${c.status}</span></td>
|
||||
<td>${new Date(c.closed_at).toLocaleString('tr-TR')}</td>
|
||||
</tr>`).join('');
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ async function api(method, path) {
|
|||
if (res.status === 401) { window.location.href = '/'; throw new Error('Unauthorized'); }
|
||||
return res;
|
||||
}
|
||||
function fmt(n, d=2) { return parseFloat(n).toLocaleString('tr-TR', { minimumFractionDigits: d, maximumFractionDigits: d }); }
|
||||
function fmt(n, d=8) { const v = parseFloat(n); if (isNaN(v)) return '—'; return v.toLocaleString('tr-TR', { minimumFractionDigits: 0, maximumFractionDigits: d }); }
|
||||
function ts(ms) { return new Date(ms).toLocaleString('tr-TR'); }
|
||||
|
||||
function onModeChange() { loadOpen(); loadClosed(); }
|
||||
|
|
@ -114,9 +114,9 @@ function renderOpen() {
|
|||
tbody.innerHTML = page.map(p => `<tr>
|
||||
<td>${p.bot_name}</td>
|
||||
<td>${p.symbol}</td>
|
||||
<td>${fmt(p.buy_price, 4)}</td>
|
||||
<td>${fmt(p.sell_target, 4)}</td>
|
||||
<td>${fmt(p.quantity, 4)}</td>
|
||||
<td>${fmt(p.buy_price)}</td>
|
||||
<td>${fmt(p.sell_target)}</td>
|
||||
<td>${fmt(p.quantity)}</td>
|
||||
<td>${ts(p.opened_at)}</td>
|
||||
</tr>`).join('');
|
||||
renderPagination('open-pagination', openCache.length, openPage, 'goOpenPage');
|
||||
|
|
@ -129,9 +129,9 @@ function renderClosed() {
|
|||
tbody.innerHTML = page.map(p => `<tr>
|
||||
<td>${p.bot_name}</td>
|
||||
<td>${p.symbol}</td>
|
||||
<td>${fmt(p.buy_price, 4)}</td>
|
||||
<td>${fmt(p.sell_target, 4)}</td>
|
||||
<td>${fmt(p.quantity, 4)}</td>
|
||||
<td>${fmt(p.buy_price)}</td>
|
||||
<td>${fmt(p.sell_target)}</td>
|
||||
<td>${fmt(p.quantity)}</td>
|
||||
<td><span class="badge badge-${p.status.toLowerCase()}">${p.status}</span></td>
|
||||
<td>${ts(p.closed_at)}</td>
|
||||
</tr>`).join('');
|
||||
|
|
|
|||
|
|
@ -49,18 +49,13 @@
|
|||
tr:last-child td { border-bottom: none; }
|
||||
tr:hover td { background: rgba(255,255,255,.02); }
|
||||
.asset-name { font-weight: 600; font-size: 13px; }
|
||||
.asset-full { font-size: 11px; color: var(--muted); margin-top: 1px; }
|
||||
.val-free { color: var(--text); }
|
||||
.val-locked { color: var(--yellow); }
|
||||
.val-usdt { font-size: 11px; color: var(--muted); margin-top: 1px; }
|
||||
.val-pnl-pos { color: var(--green); }
|
||||
.val-pnl-neg { color: var(--red); }
|
||||
.empty { padding: 32px 18px; color: var(--muted); font-size: 13px; text-align: center; }
|
||||
.loading { padding: 32px 18px; color: var(--muted); font-size: 13px; text-align: center; }
|
||||
.usdt-row td { background: rgba(46,204,113,.04); }
|
||||
.usdt-row .asset-name { color: var(--green); }
|
||||
|
||||
.coming-soon { padding: 48px 18px; text-align: center; color: var(--muted); }
|
||||
.coming-soon .icon { font-size: 32px; margin-bottom: 12px; }
|
||||
.coming-soon p { font-size: 13px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -71,9 +66,9 @@
|
|||
<div class="page-toolbar">
|
||||
<span class="page-title">Cüzdanlar</span>
|
||||
<div class="tab-bar">
|
||||
<button class="tab active" onclick="showTab('spot')">Spot</button>
|
||||
<button class="tab" onclick="showTab('earn')">Earn</button>
|
||||
<button class="tab" onclick="showTab('futures')">Futures</button>
|
||||
<button class="tab active" id="tab-btn-spot" onclick="showTab('spot')">Spot</button>
|
||||
<button class="tab" id="tab-btn-earn" onclick="showTab('earn')">Earn</button>
|
||||
<button class="tab" id="tab-btn-futures" onclick="showTab('futures')">Futures</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -92,40 +87,69 @@
|
|||
|
||||
<div id="tab-earn" style="display:none">
|
||||
<section>
|
||||
<div class="section-header"><span class="section-title">Earn Cüzdan</span></div>
|
||||
<div class="coming-soon">
|
||||
<div class="icon">🔒</div>
|
||||
<p>Earn cüzdanı desteği yakında eklenecek.</p>
|
||||
<div class="section-header">
|
||||
<span class="section-title">Simple Earn — Flexible</span>
|
||||
<span class="total-badge" id="earn-total">—</span>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>Varlık</th><th>Toplam</th><th>Kullanılabilir</th></tr></thead>
|
||||
<tbody id="earn-body"><tr><td colspan="3" class="loading">Yükleniyor...</td></tr></tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="tab-futures" style="display:none">
|
||||
<section>
|
||||
<div class="section-header"><span class="section-title">Futures Cüzdan</span></div>
|
||||
<div class="coming-soon">
|
||||
<div class="icon">📈</div>
|
||||
<p>Futures cüzdanı desteği yakında eklenecek.</p>
|
||||
<div class="section-header">
|
||||
<span class="section-title">USD-M Futures</span>
|
||||
<span class="total-badge" id="futures-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-body"><tr><td colspan="4" class="loading">Yükleniyor...</td></tr></tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function onModeChange() { loadSpot(); }
|
||||
function onModeChange() { loadActive(); }
|
||||
|
||||
async function api(path) {
|
||||
const res = await fetch('/api' + path, { credentials: 'include' });
|
||||
if (res.status === 401) { window.location.href = '/'; throw new Error('Unauthorized'); }
|
||||
return res;
|
||||
}
|
||||
function fmt(n, d=4) { return parseFloat(n).toLocaleString('tr-TR', { minimumFractionDigits: 0, maximumFractionDigits: d }); }
|
||||
|
||||
function fmt(n) {
|
||||
const v = parseFloat(n);
|
||||
if (isNaN(v)) return '—';
|
||||
// trailing sıfırları at ama max 8 basamak göster
|
||||
return v.toLocaleString('tr-TR', { minimumFractionDigits: 0, maximumFractionDigits: 8 });
|
||||
}
|
||||
|
||||
let activeTab = 'spot';
|
||||
const loaded = { spot: false, earn: false, futures: false };
|
||||
|
||||
function showTab(name) {
|
||||
['spot','earn','futures'].forEach(t => {
|
||||
document.getElementById('tab-' + t).style.display = t === name ? '' : 'none';
|
||||
document.querySelectorAll('.tab')[['spot','earn','futures'].indexOf(t)].className = 'tab' + (t === name ? ' active' : '');
|
||||
document.getElementById('tab-btn-' + t).className = 'tab' + (t === name ? ' active' : '');
|
||||
});
|
||||
activeTab = name;
|
||||
if (!loaded[name]) loadTab(name);
|
||||
}
|
||||
|
||||
function loadActive() {
|
||||
loaded.spot = false; loaded.earn = false; loaded.futures = false;
|
||||
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;
|
||||
}
|
||||
|
||||
async function loadSpot() {
|
||||
|
|
@ -134,17 +158,10 @@ async function loadSpot() {
|
|||
document.getElementById('usdt-total').textContent = '—';
|
||||
|
||||
const res = await api('/wallet/spot');
|
||||
if (!res.ok) {
|
||||
tbody.innerHTML = `<tr><td colspan="3" class="empty">Bakiye alınamadı: ${await res.text()}</td></tr>`;
|
||||
return;
|
||||
}
|
||||
if (!res.ok) { tbody.innerHTML = `<tr><td colspan="3" class="empty">Hata: ${await res.text()}</td></tr>`; return; }
|
||||
const balances = await res.json();
|
||||
if (!balances.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="3" class="empty">Bakiye yok</td></tr>';
|
||||
return;
|
||||
}
|
||||
if (!balances.length) { tbody.innerHTML = '<tr><td colspan="3" class="empty">Bakiye yok</td></tr>'; return; }
|
||||
|
||||
// USDT'yi en üste al, sonra free'ye göre sırala
|
||||
balances.sort((a, b) => {
|
||||
if (a.asset === 'USDT') return -1;
|
||||
if (b.asset === 'USDT') return 1;
|
||||
|
|
@ -154,15 +171,58 @@ async function loadSpot() {
|
|||
const usdt = balances.find(b => b.asset === 'USDT');
|
||||
if (usdt) {
|
||||
document.getElementById('usdt-total').textContent =
|
||||
`${fmt(usdt.free, 2)} serbest / ${fmt(usdt.free + usdt.locked, 2)} USDT toplam`;
|
||||
`${fmt(usdt.free)} serbest / ${fmt(usdt.free + usdt.locked)} USDT toplam`;
|
||||
}
|
||||
|
||||
tbody.innerHTML = balances.map(b => `<tr class="${b.asset === 'USDT' ? 'usdt-row' : ''}">
|
||||
<td><span class="asset-name">${b.asset}</span></td>
|
||||
<td>${fmt(b.free)}</td>
|
||||
<td>${b.locked > 0 ? `<span class="val-locked">${fmt(b.locked)}</span>` : '<span style="color:var(--muted)">—</span>'}</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
async function loadEarn() {
|
||||
const tbody = document.getElementById('earn-body');
|
||||
tbody.innerHTML = '<tr><td colspan="3" class="loading">Yükleniyor...</td></tr>';
|
||||
document.getElementById('earn-total').textContent = '—';
|
||||
|
||||
const res = await api('/wallet/earn');
|
||||
if (!res.ok) { tbody.innerHTML = `<tr><td colspan="3" class="empty">Hata: ${await res.text()}</td></tr>`; return; }
|
||||
const balances = await res.json();
|
||||
if (!balances.length) { tbody.innerHTML = '<tr><td colspan="3" class="empty">Earn pozisyonu yok</td></tr>'; return; }
|
||||
|
||||
balances.sort((a, b) => b.total - a.total);
|
||||
document.getElementById('earn-total').textContent = `${balances.length} varlık`;
|
||||
|
||||
tbody.innerHTML = balances.map(b => `<tr>
|
||||
<td><span class="asset-name">${b.asset}</span></td>
|
||||
<td>${fmt(b.total)}</td>
|
||||
<td>${fmt(b.free)}</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
async function loadFutures() {
|
||||
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);
|
||||
const totalWallet = balances.reduce((s, b) => s + b.wallet, 0);
|
||||
document.getElementById('futures-total').textContent = `${fmt(totalWallet)} USDT toplam`;
|
||||
|
||||
tbody.innerHTML = balances.map(b => {
|
||||
const isUsdt = b.asset === 'USDT';
|
||||
return `<tr class="${isUsdt ? 'usdt-row' : ''}">
|
||||
<td><div class="asset-name">${b.asset}</div></td>
|
||||
<td><div class="val-free">${fmt(b.free)}</div></td>
|
||||
<td><div class="val-locked${b.locked > 0 ? '' : ''}">${b.locked > 0 ? fmt(b.locked) : '<span style="color:var(--muted)">—</span>'}</div></td>
|
||||
const pnlClass = b.unrealized_pnl >= 0 ? 'val-pnl-pos' : 'val-pnl-neg';
|
||||
const pnlSign = b.unrealized_pnl >= 0 ? '+' : '';
|
||||
return `<tr>
|
||||
<td><span class="asset-name">${b.asset}</span></td>
|
||||
<td>${fmt(b.wallet)}</td>
|
||||
<td><span class="${pnlClass}">${pnlSign}${fmt(b.unrealized_pnl)}</span></td>
|
||||
<td>${fmt(b.available)}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
|
@ -172,7 +232,8 @@ async function logout() {
|
|||
window.location.href = '/';
|
||||
}
|
||||
|
||||
loadSpot();
|
||||
loadTab('spot');
|
||||
loaded.spot = true;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue