feat: 手续费改为逐笔USD累算 + Vite React前端 + system_orders表 + README
This commit is contained in:
+2
-240
@@ -23,11 +23,6 @@ const els = {
|
||||
statFlat: $('stat-flat'),
|
||||
statPos: $('stat-positions'),
|
||||
statCoins: $('stat-coins'),
|
||||
chartCoin: $('chart-coin'),
|
||||
chartExch: $('chart-exchange'),
|
||||
chartCanvas: $('priceChart'),
|
||||
spreadCoin: $('spread-coin'),
|
||||
spreadCanvas: $('spreadChart'),
|
||||
};
|
||||
|
||||
// ---- Clock ----
|
||||
@@ -58,7 +53,6 @@ function pnlClass(val) {
|
||||
return val > 0 ? 'text-green' : val < 0 ? 'text-red' : '';
|
||||
}
|
||||
|
||||
// ---- Price cache for chart data ----
|
||||
const priceCache = {};
|
||||
|
||||
// ---- SSE Connection ----
|
||||
@@ -125,15 +119,7 @@ eventHandlers.prices = (prices) => {
|
||||
if (prev) {
|
||||
prev.last = curP;
|
||||
} else {
|
||||
priceCache[key] = { last: curP, points: [] };
|
||||
}
|
||||
|
||||
if (p > 0) {
|
||||
if (!priceCache[key]) priceCache[key] = { last: p, points: [] };
|
||||
priceCache[key].points.push({ t: Date.now(), p: p });
|
||||
if (priceCache[key].points.length > 500) {
|
||||
priceCache[key].points = priceCache[key].points.slice(-500);
|
||||
}
|
||||
priceCache[key] = { last: curP };
|
||||
}
|
||||
|
||||
let display = formatPrice(p);
|
||||
@@ -150,8 +136,6 @@ eventHandlers.prices = (prices) => {
|
||||
|
||||
els.priceBody.innerHTML = html;
|
||||
els.pricesAge.textContent = new Date().toLocaleTimeString('zh-CN', { hour12: false });
|
||||
|
||||
updateChartSelectors(prices);
|
||||
};
|
||||
|
||||
eventHandlers.arb = (opps) => {
|
||||
@@ -268,226 +252,6 @@ eventHandlers.trade_close = (trade) => {
|
||||
setTimeout(loadTrades, 500);
|
||||
};
|
||||
|
||||
// ---- Price Chart ----
|
||||
let priceChart = null;
|
||||
|
||||
function initPriceChart() {
|
||||
const ctx = els.chartCanvas.getContext('2d');
|
||||
priceChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: { datasets: [{
|
||||
label: 'Price',
|
||||
data: [],
|
||||
borderColor: '#58a6ff',
|
||||
backgroundColor: 'rgba(88, 166, 255, 0.1)',
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
fill: true,
|
||||
tension: 0.2,
|
||||
}] },
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: { duration: 0 },
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
mode: 'index', intersect: false,
|
||||
callbacks: {
|
||||
title: (items) => items.length ? new Date(items[0].parsed.x).toLocaleTimeString('zh-CN', { hour12: false }) : '',
|
||||
label: (item) => item.parsed.y.toFixed(4),
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'linear',
|
||||
ticks: {
|
||||
color: '#8b949e', maxTicksLimit: 10,
|
||||
callback: (v) => new Date(v).toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit' }),
|
||||
},
|
||||
grid: { color: 'rgba(48,54,61,0.5)' },
|
||||
},
|
||||
y: {
|
||||
ticks: { color: '#8b949e', callback: (v) => v.toFixed(4) },
|
||||
grid: { color: 'rgba(48,54,61,0.3)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ---- P3-2: Spread Chart ----
|
||||
let spreadChart = null;
|
||||
|
||||
function initSpreadChart() {
|
||||
const ctx = els.spreadCanvas.getContext('2d');
|
||||
spreadChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: { datasets: [{
|
||||
label: 'BG↔HL Spread %',
|
||||
data: [],
|
||||
borderColor: '#d29922',
|
||||
backgroundColor: 'rgba(210, 153, 34, 0.1)',
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
fill: true,
|
||||
tension: 0.2,
|
||||
}] },
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: { duration: 0 },
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
mode: 'index', intersect: false,
|
||||
callbacks: {
|
||||
title: (items) => items.length ? new Date(items[0].parsed.x).toLocaleTimeString('zh-CN', { hour12: false }) : '',
|
||||
label: (item) => item.parsed.y.toFixed(4) + '%',
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'linear',
|
||||
ticks: {
|
||||
color: '#8b949e', maxTicksLimit: 10,
|
||||
callback: (v) => new Date(v).toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit' }),
|
||||
},
|
||||
grid: { color: 'rgba(48,54,61,0.5)' },
|
||||
},
|
||||
y: {
|
||||
ticks: { color: '#8b949e', callback: (v) => v.toFixed(3) + '%' },
|
||||
grid: { color: 'rgba(48,54,61,0.3)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Chart Selectors ----
|
||||
function updateChartSelectors(prices) {
|
||||
const coinSel = els.chartCoin;
|
||||
const exSel = els.chartExch;
|
||||
const spreadSel = els.spreadCoin;
|
||||
|
||||
// Price chart coin selector
|
||||
if (coinSel.options.length <= 1) {
|
||||
const cur = coinSel.value;
|
||||
coinSel.innerHTML = '<option value="">-- 选择币种 --</option>';
|
||||
for (const row of prices) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = row.coin; opt.textContent = row.coin;
|
||||
coinSel.appendChild(opt);
|
||||
}
|
||||
if (cur) coinSel.value = cur;
|
||||
else if (prices.length > 0) coinSel.value = prices[0].coin;
|
||||
}
|
||||
|
||||
// Price chart exchange selector
|
||||
if (exSel.options.length <= 1) {
|
||||
exSel.innerHTML = '<option value="">-- 选择交易所 --</option>';
|
||||
for (const ex of EXCHANGES) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = ex; opt.textContent = ex;
|
||||
exSel.appendChild(opt);
|
||||
}
|
||||
exSel.value = 'HyperLiquid';
|
||||
}
|
||||
|
||||
// Spread chart coin selector
|
||||
if (spreadSel.options.length <= 1) {
|
||||
const cur = spreadSel.value;
|
||||
spreadSel.innerHTML = '<option value="">-- 选择币种 --</option>';
|
||||
for (const row of prices) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = row.coin; opt.textContent = row.coin;
|
||||
spreadSel.appendChild(opt);
|
||||
}
|
||||
if (cur) spreadSel.value = cur;
|
||||
else if (prices.length > 0) spreadSel.value = prices[0].coin;
|
||||
}
|
||||
|
||||
// Update charts on selection change
|
||||
const selCoin = coinSel.value, selEx = exSel.value;
|
||||
if (selCoin && selEx) updatePriceChart(selCoin, selEx);
|
||||
|
||||
const spCoin = spreadSel.value;
|
||||
if (spCoin) updateSpreadChart(spCoin);
|
||||
}
|
||||
|
||||
function updatePriceChart(coin, exchange) {
|
||||
const key = coin + '.' + exchange;
|
||||
const cache = priceCache[key];
|
||||
if (!cache || !cache.points || cache.points.length < 2) {
|
||||
if (priceChart) {
|
||||
priceChart.data.datasets[0].data = [];
|
||||
priceChart.data.datasets[0].label = `${coin} @ ${exchange}`;
|
||||
priceChart.update('none');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const data = cache.points.map(p => ({ x: p.t, y: p.p }));
|
||||
if (priceChart) {
|
||||
priceChart.data.datasets[0].data = data;
|
||||
priceChart.data.datasets[0].label = `${coin} @ ${exchange}`;
|
||||
priceChart.update('none');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSpreadChart(coin) {
|
||||
try {
|
||||
const resp = await fetch(`/api/spread-history?coin=${coin}`);
|
||||
const data = await resp.json();
|
||||
const pts = data.points || [];
|
||||
if (pts.length < 2) {
|
||||
if (spreadChart) {
|
||||
spreadChart.data.datasets[0].data = [];
|
||||
spreadChart.data.datasets[0].label = `${coin} BG↔HL`;
|
||||
spreadChart.update('none');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const chartData = pts.map(p => ({ x: p.t, y: p.s }));
|
||||
if (spreadChart) {
|
||||
spreadChart.data.datasets[0].data = chartData;
|
||||
spreadChart.data.datasets[0].label = `${coin} BG↔HL`;
|
||||
spreadChart.update('none');
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Chart controls ----
|
||||
els.chartCoin.addEventListener('change', () => {
|
||||
const coin = els.chartCoin.value;
|
||||
const ex = els.chartExch.value;
|
||||
if (coin && ex) updatePriceChart(coin, ex);
|
||||
});
|
||||
|
||||
els.chartExch.addEventListener('change', () => {
|
||||
const coin = els.chartCoin.value;
|
||||
const ex = els.chartExch.value;
|
||||
if (coin && ex) updatePriceChart(coin, ex);
|
||||
});
|
||||
|
||||
els.spreadCoin.addEventListener('change', () => {
|
||||
if (els.spreadCoin.value) updateSpreadChart(els.spreadCoin.value);
|
||||
});
|
||||
|
||||
// ---- Auto-refresh charts ----
|
||||
setInterval(() => {
|
||||
const coin = els.chartCoin.value;
|
||||
const ex = els.chartExch.value;
|
||||
if (coin && ex) updatePriceChart(coin, ex);
|
||||
}, 2000);
|
||||
|
||||
setInterval(() => {
|
||||
if (els.spreadCoin.value) updateSpreadChart(els.spreadCoin.value);
|
||||
}, 3000);
|
||||
|
||||
// ---- Trades from API ----
|
||||
async function loadTrades() {
|
||||
try {
|
||||
@@ -643,10 +407,8 @@ window.closeTradeDetail = closeTradeDetail;
|
||||
// ---- Init ----
|
||||
function init() {
|
||||
connectSSE();
|
||||
loadTrades(); // run before charts in case Chart CDN is slow
|
||||
loadTrades();
|
||||
setInterval(loadTrades, 10000);
|
||||
try { initPriceChart(); } catch(e) { console.warn('Price chart init failed:', e); }
|
||||
try { initSpreadChart(); } catch(e) { console.warn('Spread chart init failed:', e); }
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
|
||||
+18
-39
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Exchange Monitor Dashboard</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
@@ -42,6 +42,21 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Open Positions -->
|
||||
<section class="card" id="positions-card">
|
||||
<h2>🔒 当前持仓</h2>
|
||||
<div class="table-wrap">
|
||||
<table id="positions-table">
|
||||
<thead>
|
||||
<tr><th>币种</th><th>方向</th><th>规模</th><th>入价差</th><th>现价差</th><th>估盈亏</th><th>加仓</th><th>时长</th></tr>
|
||||
</thead>
|
||||
<tbody id="positions-body">
|
||||
<tr><td colspan="8" class="loading">等待数据...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Blacklist -->
|
||||
<section class="card" id="bl-card">
|
||||
<h2>⛔ 黑名单</h2>
|
||||
@@ -80,44 +95,6 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Open Positions -->
|
||||
<section class="card" id="positions-card">
|
||||
<h2>🔒 当前持仓</h2>
|
||||
<div class="table-wrap">
|
||||
<table id="positions-table">
|
||||
<thead>
|
||||
<tr><th>币种</th><th>方向</th><th>规模</th><th>入价差</th><th>现价差</th><th>估盈亏</th><th>加仓</th><th>时长</th></tr>
|
||||
</thead>
|
||||
<tbody id="positions-body">
|
||||
<tr><td colspan="8" class="loading">等待数据...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Price Chart -->
|
||||
<section class="card card-wide" id="chart-card">
|
||||
<h2>📈 价格走势</h2>
|
||||
<div class="chart-controls">
|
||||
<select id="chart-coin"></select>
|
||||
<select id="chart-exchange"></select>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas id="priceChart"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Spread Chart (P3-2) -->
|
||||
<section class="card card-wide" id="spread-chart-card">
|
||||
<h2>📉 价差走势 (BG↔HL)</h2>
|
||||
<div class="chart-controls">
|
||||
<select id="spread-coin"></select>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas id="spreadChart"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Recent Trades -->
|
||||
<section class="card card-wide" id="trades-card">
|
||||
<h2>📋 历史交易</h2>
|
||||
@@ -132,6 +109,8 @@
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Trade Detail Modal -->
|
||||
|
||||
@@ -135,26 +135,6 @@ tr:hover td { background: rgba(88, 166, 255, 0.05); }
|
||||
.text-dim { color: var(--text-dim); }
|
||||
.text-right { text-align: right; }
|
||||
|
||||
/* Chart controls */
|
||||
.chart-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.chart-controls select {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
|
||||
Reference in New Issue
Block a user