Phase 3: Real-time enhancements
P3-1: Scan optimization — only BG↔HL (50+ pair combos → 2)
P3-2: Real-time spread chart — spreadHistory ring buffer +
/api/spread-history endpoint + Chart.js spread chart
P3-3: Live position PnL — positions SSE now includes
estimated current profit/loss + current spread
P3-4: Real-time trade events — trader.OnTradeEvent callback
fires SSE 'trade_open' / 'trade_close' immediately
P3-5: Connection status monitoring — tracks last update time
per exchange, broadcast via stats.connections + /api/connections
Frontend: spread chart card, PnL column in positions,
connection status dots in stats bar,
green/red border flash on trade events
This commit is contained in:
+241
-139
@@ -1,5 +1,5 @@
|
||||
/* ============================================================
|
||||
Exchange Monitor Dashboard — Frontend Logic
|
||||
Exchange Monitor Dashboard — Frontend Logic v3 (P3)
|
||||
============================================================ */
|
||||
|
||||
(function() {
|
||||
@@ -9,21 +9,25 @@
|
||||
const $ = id => document.getElementById(id);
|
||||
|
||||
const els = {
|
||||
clock: $('clock'),
|
||||
connStatus: $('conn-status'),
|
||||
priceBody: $('price-body'),
|
||||
arbBody: $('arb-body'),
|
||||
posBody: $('positions-body'),
|
||||
tradesBody: $('trades-body'),
|
||||
statTotal: $('stat-total'),
|
||||
statConv: $('stat-converged'),
|
||||
statDiv: $('stat-diverged'),
|
||||
statFlat: $('stat-flat'),
|
||||
statPos: $('stat-positions'),
|
||||
statCoins: $('stat-coins'),
|
||||
chartCoin: $('chart-coin'),
|
||||
chartExch: $('chart-exchange'),
|
||||
chartCanvas: $('priceChart'),
|
||||
clock: $('clock'),
|
||||
connStatus: $('conn-status'),
|
||||
connDetail: $('conn-detail'),
|
||||
pricesAge: $('prices-age'),
|
||||
priceBody: $('price-body'),
|
||||
arbBody: $('arb-body'),
|
||||
posBody: $('positions-body'),
|
||||
tradesBody: $('trades-body'),
|
||||
statTotal: $('stat-total'),
|
||||
statConv: $('stat-converged'),
|
||||
statDiv: $('stat-diverged'),
|
||||
statFlat: $('stat-flat'),
|
||||
statPos: $('stat-positions'),
|
||||
statCoins: $('stat-coins'),
|
||||
chartCoin: $('chart-coin'),
|
||||
chartExch: $('chart-exchange'),
|
||||
chartCanvas: $('priceChart'),
|
||||
spreadCoin: $('spread-coin'),
|
||||
spreadCanvas: $('spreadChart'),
|
||||
};
|
||||
|
||||
// ---- Clock ----
|
||||
@@ -34,7 +38,6 @@ function updateClock() {
|
||||
setInterval(updateClock, 1000);
|
||||
updateClock();
|
||||
|
||||
// ---- Price table helpers ----
|
||||
const EXCHANGES = ['Binance', 'HyperLiquid', 'Bitget', 'dYdX'];
|
||||
const COINS = ['DOGE', 'LINK', 'ONDO', 'OP', 'WIF', 'ARB'];
|
||||
|
||||
@@ -45,23 +48,24 @@ function formatPrice(p) {
|
||||
return p.toFixed(6);
|
||||
}
|
||||
|
||||
function priceClass(lastPrice, currentPrice) {
|
||||
if (lastPrice == null || currentPrice == null) return '';
|
||||
if (currentPrice > lastPrice) return 'text-green';
|
||||
if (currentPrice < lastPrice) return 'text-red';
|
||||
return '';
|
||||
function priceClass(last, cur) {
|
||||
if (last == null || cur == null) return '';
|
||||
return cur > last ? 'text-green' : cur < last ? 'text-red' : '';
|
||||
}
|
||||
|
||||
// ---- Price history for chart ----
|
||||
const priceCache = {}; // coin.exchange -> { last: float, points: [{t,p}] }
|
||||
function pnlClass(val) {
|
||||
if (val == null) return '';
|
||||
return val > 0 ? 'text-green' : val < 0 ? 'text-red' : '';
|
||||
}
|
||||
|
||||
// ---- Price cache for chart data ----
|
||||
const priceCache = {};
|
||||
|
||||
// ---- SSE Connection ----
|
||||
let eventSource = null;
|
||||
|
||||
function connectSSE() {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
}
|
||||
if (eventSource) eventSource.close();
|
||||
|
||||
eventSource = new EventSource('/events');
|
||||
|
||||
@@ -93,14 +97,13 @@ const eventHandlers = {};
|
||||
eventHandlers.prices = (prices) => {
|
||||
if (!prices || prices.length === 0) return;
|
||||
|
||||
// Build table rows
|
||||
let html = '';
|
||||
let coinsOnline = 0;
|
||||
|
||||
for (const coin of COINS) {
|
||||
const row = prices.find(p => p.coin === coin);
|
||||
if (!row) {
|
||||
html += `<tr><td>${coin}</td>${EXCHANGES.map(() => '<td class="text-dim">-</td>').join('')}</tr>`;
|
||||
html += `<tr><td>${coin}</td>${EXCHANGES.map(() => '<td class="text-dim">-</td>').join('')}<td class="text-dim">-</td></tr>`;
|
||||
continue;
|
||||
}
|
||||
coinsOnline++;
|
||||
@@ -113,18 +116,15 @@ eventHandlers.prices = (prices) => {
|
||||
const curP = p || 0;
|
||||
const cls = prev ? priceClass(prev.last, curP) : '';
|
||||
|
||||
// Store for directional arrows next time
|
||||
if (prev) {
|
||||
prev.last = curP;
|
||||
} else {
|
||||
priceCache[key] = { last: curP, points: [] };
|
||||
}
|
||||
|
||||
// Record for chart
|
||||
if (p > 0) {
|
||||
const pt = { t: Date.now(), p: p };
|
||||
if (!priceCache[key]) priceCache[key] = { last: p, points: [] };
|
||||
priceCache[key].points.push(pt);
|
||||
priceCache[key].points.push({ t: Date.now(), p: p });
|
||||
if (priceCache[key].points.length > 500) {
|
||||
priceCache[key].points = priceCache[key].points.slice(-500);
|
||||
}
|
||||
@@ -137,12 +137,17 @@ eventHandlers.prices = (prices) => {
|
||||
return `<td class="${cls}">${display}</td>`;
|
||||
});
|
||||
|
||||
html += `<tr><td><strong>${coin}</strong></td>${cells.join('')}</tr>`;
|
||||
// P3-2: Add spread column
|
||||
const spread = row['bg_hl_spread'];
|
||||
const spreadCls = spread > 0.1 ? 'text-green' : spread < -0.1 ? 'text-red' : '';
|
||||
const spreadStr = spread != null ? spread.toFixed(4) + '%' : '-';
|
||||
|
||||
html += `<tr><td><strong>${coin}</strong></td>${cells.join('')}<td class="${spreadCls}">${spreadStr}</td></tr>`;
|
||||
}
|
||||
|
||||
els.priceBody.innerHTML = html;
|
||||
els.pricesAge.textContent = new Date().toLocaleTimeString('zh-CN', { hour12: false });
|
||||
|
||||
// Update coin selector if needed
|
||||
updateChartSelectors(prices);
|
||||
};
|
||||
|
||||
@@ -166,24 +171,33 @@ eventHandlers.arb = (opps) => {
|
||||
els.arbBody.innerHTML = html;
|
||||
};
|
||||
|
||||
// P3-3: Positions with live PnL
|
||||
eventHandlers.positions = (positions) => {
|
||||
if (!positions || positions.length === 0) {
|
||||
els.posBody.innerHTML = '<tr><td colspan="6" class="text-dim">无持仓</td></tr>';
|
||||
els.posBody.innerHTML = '<tr><td colspan="8" class="text-dim">无持仓</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
const html = positions.map(p => `<tr>
|
||||
<td><strong>${p.coin}</strong></td>
|
||||
<td>${p.direction}</td>
|
||||
<td class="text-right">$${p.amount_usd.toFixed(0)}</td>
|
||||
<td class="text-right">${p.entry_spread.toFixed(4)}%</td>
|
||||
<td class="text-right">${p.scales}</td>
|
||||
<td>${p.duration}</td>
|
||||
</tr>`).join('');
|
||||
const html = positions.map(p => {
|
||||
const pnl = p.pnl_est;
|
||||
const pnlStr = pnl != null ? pnl.toFixed(4) + '%' : '-';
|
||||
const curSpread = p.current_spread != null ? p.current_spread.toFixed(4) + '%' : '-';
|
||||
return `<tr>
|
||||
<td><strong>${p.coin}</strong></td>
|
||||
<td>${p.direction}</td>
|
||||
<td class="text-right">$${p.amount_usd.toFixed(0)}</td>
|
||||
<td class="text-right">${p.entry_spread.toFixed(4)}%</td>
|
||||
<td class="text-right">${curSpread}</td>
|
||||
<td class="text-right ${pnlClass(pnl)}"><strong>${pnlStr}</strong></td>
|
||||
<td class="text-right">${p.scales}</td>
|
||||
<td>${p.duration}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
els.posBody.innerHTML = html;
|
||||
};
|
||||
|
||||
// P3-5: Connection status in stats
|
||||
eventHandlers.stats = (stats) => {
|
||||
els.statTotal.textContent = stats.total_trades || 0;
|
||||
els.statConv.textContent = stats.converged || 0;
|
||||
@@ -191,27 +205,53 @@ eventHandlers.stats = (stats) => {
|
||||
els.statFlat.textContent = stats.flat || 0;
|
||||
els.statPos.textContent = stats.open_positions || 0;
|
||||
els.statCoins.textContent = stats.coins || 0;
|
||||
|
||||
// Connection status dots
|
||||
if (stats.connections) {
|
||||
const dots = Object.entries(stats.connections).map(([ex, status]) => {
|
||||
const color = status === 'online' ? '#3fb950' : status === 'stale' ? '#d29922' : '#f85149';
|
||||
return `<span style="color:${color}">●</span> ${ex}`;
|
||||
}).join(' ');
|
||||
els.connDetail.innerHTML = dots;
|
||||
}
|
||||
};
|
||||
|
||||
// ---- Chart ----
|
||||
let chart = null;
|
||||
// P3-4: Real-time trade events
|
||||
eventHandlers.trade_open = (trade) => {
|
||||
// Flash the positions card to draw attention
|
||||
const card = $('positions-card');
|
||||
card.style.transition = 'border-color 0.3s';
|
||||
card.style.borderColor = '#3fb950';
|
||||
setTimeout(() => { card.style.borderColor = ''; }, 2000);
|
||||
// Refresh trades table
|
||||
setTimeout(loadTrades, 500);
|
||||
};
|
||||
|
||||
function initChart() {
|
||||
eventHandlers.trade_close = (trade) => {
|
||||
const card = $('trades-card');
|
||||
card.style.transition = 'border-color 0.3s';
|
||||
card.style.borderColor = trade.pnl_pct > 0 ? '#3fb950' : '#f85149';
|
||||
setTimeout(() => { card.style.borderColor = ''; }, 2000);
|
||||
setTimeout(loadTrades, 500);
|
||||
};
|
||||
|
||||
// ---- Price Chart ----
|
||||
let priceChart = null;
|
||||
|
||||
function initPriceChart() {
|
||||
const ctx = els.chartCanvas.getContext('2d');
|
||||
chart = new Chart(ctx, {
|
||||
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,
|
||||
}]
|
||||
},
|
||||
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,
|
||||
@@ -219,108 +259,171 @@ function initChart() {
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
mode: 'index', intersect: false,
|
||||
callbacks: {
|
||||
title: (items) => {
|
||||
if (items.length > 0) {
|
||||
const d = new Date(items[0].parsed.x);
|
||||
return d.toLocaleTimeString('zh-CN', { hour12: false });
|
||||
}
|
||||
return '';
|
||||
},
|
||||
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',
|
||||
display: true,
|
||||
ticks: {
|
||||
color: '#8b949e',
|
||||
maxTicksLimit: 10,
|
||||
callback: (val) => {
|
||||
const d = new Date(val);
|
||||
return d.toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
}
|
||||
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)' },
|
||||
grid: { color: 'rgba(48,54,61,0.5)' },
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
ticks: {
|
||||
color: '#8b949e',
|
||||
callback: (val) => val.toFixed(4),
|
||||
},
|
||||
grid: { color: 'rgba(48, 54, 61, 0.3)' },
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
// Populate coins if empty
|
||||
// Price chart coin selector
|
||||
if (coinSel.options.length <= 1) {
|
||||
const currentCoin = coinSel.value;
|
||||
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;
|
||||
opt.value = row.coin; opt.textContent = row.coin;
|
||||
coinSel.appendChild(opt);
|
||||
}
|
||||
// Try to restore selection
|
||||
if (currentCoin) {
|
||||
coinSel.value = currentCoin;
|
||||
} else if (prices.length > 0) {
|
||||
coinSel.value = prices[0].coin;
|
||||
}
|
||||
if (cur) coinSel.value = cur;
|
||||
else if (prices.length > 0) coinSel.value = prices[0].coin;
|
||||
}
|
||||
|
||||
// Populate exchanges if empty
|
||||
// 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;
|
||||
opt.value = ex; opt.textContent = ex;
|
||||
exSel.appendChild(opt);
|
||||
}
|
||||
exSel.value = 'HyperLiquid';
|
||||
}
|
||||
|
||||
// Update chart when selections change
|
||||
const selectedCoin = coinSel.value;
|
||||
const selectedEx = exSel.value;
|
||||
if (selectedCoin && selectedEx) {
|
||||
updateChart(selectedCoin, selectedEx);
|
||||
// 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 updateChart(coin, exchange) {
|
||||
function updatePriceChart(coin, exchange) {
|
||||
const key = coin + '.' + exchange;
|
||||
const cache = priceCache[key];
|
||||
if (!cache || !cache.points || cache.points.length < 2) {
|
||||
if (chart) {
|
||||
chart.data.datasets[0].data = [];
|
||||
chart.data.datasets[0].label = `${coin} @ ${exchange}`;
|
||||
chart.update('none');
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
const pts = cache.points;
|
||||
const data = pts.map(p => ({ x: p.t, y: p.p }));
|
||||
|
||||
if (chart) {
|
||||
chart.data.datasets[0].data = data;
|
||||
chart.data.datasets[0].label = `${coin} @ ${exchange}`;
|
||||
chart.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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,29 +431,31 @@ function updateChart(coin, exchange) {
|
||||
els.chartCoin.addEventListener('change', () => {
|
||||
const coin = els.chartCoin.value;
|
||||
const ex = els.chartExch.value;
|
||||
if (coin && ex) updateChart(coin, ex);
|
||||
if (coin && ex) updatePriceChart(coin, ex);
|
||||
});
|
||||
|
||||
els.chartExch.addEventListener('change', () => {
|
||||
const coin = els.chartCoin.value;
|
||||
const ex = els.chartExch.value;
|
||||
if (coin && ex) updateChart(coin, ex);
|
||||
if (coin && ex) updatePriceChart(coin, ex);
|
||||
});
|
||||
|
||||
// ---- Chart auto-refresh ----
|
||||
let chartRefreshTimer = null;
|
||||
let chartRefreshInterval = 2000; // refresh chart every 2s
|
||||
els.spreadCoin.addEventListener('change', () => {
|
||||
if (els.spreadCoin.value) updateSpreadChart(els.spreadCoin.value);
|
||||
});
|
||||
|
||||
function startChartRefresh() {
|
||||
if (chartRefreshTimer) return;
|
||||
chartRefreshTimer = setInterval(() => {
|
||||
const coin = els.chartCoin.value;
|
||||
const ex = els.chartExch.value;
|
||||
if (coin && ex) updateChart(coin, ex);
|
||||
}, chartRefreshInterval);
|
||||
}
|
||||
// ---- Auto-refresh charts ----
|
||||
setInterval(() => {
|
||||
const coin = els.chartCoin.value;
|
||||
const ex = els.chartExch.value;
|
||||
if (coin && ex) updatePriceChart(coin, ex);
|
||||
}, 2000);
|
||||
|
||||
// ---- Trades loading ----
|
||||
setInterval(() => {
|
||||
if (els.spreadCoin.value) updateSpreadChart(els.spreadCoin.value);
|
||||
}, 3000);
|
||||
|
||||
// ---- Trades from API ----
|
||||
async function loadTrades() {
|
||||
try {
|
||||
const resp = await fetch('/api/trades');
|
||||
@@ -370,8 +475,8 @@ async function loadTrades() {
|
||||
<td class="text-dim">${t.ClosedAt ? new Date(t.ClosedAt).toLocaleTimeString('zh-CN', { hour12: false }) : '-'}</td>
|
||||
<td><strong>${t.Coin}</strong></td>
|
||||
<td>${t.Direction}</td>
|
||||
<td class="text-right">${t.EntrySpread ? t.EntrySpread.toFixed(4) : '-'}</td>
|
||||
<td class="text-right">${t.ExitSpread ? t.ExitSpread.toFixed(4) : '-'}</td>
|
||||
<td class="text-right">${t.EntrySpread != null ? t.EntrySpread.toFixed(4) : '-'}</td>
|
||||
<td class="text-right">${t.ExitSpread != null ? t.ExitSpread.toFixed(4) : '-'}</td>
|
||||
<td class="text-right ${pnlCls}"><strong>${t.NetPnl != null ? t.NetPnl.toFixed(4) : '-'}</strong></td>
|
||||
<td class="${convCls}">${t.Convergence || '-'}</td>
|
||||
<td>${t.ExitReason || '-'}</td>
|
||||
@@ -387,15 +492,12 @@ async function loadTrades() {
|
||||
// ---- Init ----
|
||||
function init() {
|
||||
connectSSE();
|
||||
initChart();
|
||||
startChartRefresh();
|
||||
initPriceChart();
|
||||
initSpreadChart();
|
||||
loadTrades();
|
||||
|
||||
// Refresh trades every 10s
|
||||
setInterval(loadTrades, 10000);
|
||||
}
|
||||
|
||||
// Start when DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user