Files
gogo2/web/cob_dashboard.html
Dobromir Popov 3cadae60f7 COB data and dash
2025-06-18 16:23:47 +03:00

689 lines
22 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Consolidated Order Book Dashboard</title>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<style>
body {
font-family: 'Arial', sans-serif;
margin: 0;
padding: 15px;
background-color: #0a0a0a;
color: #ffffff;
overflow-x: auto;
}
.header {
text-align: center;
margin-bottom: 15px;
}
.header h1 {
color: #00ff88;
margin: 0;
font-size: 1.8rem;
}
.header .subtitle {
color: #888;
font-size: 0.9rem;
margin-top: 3px;
}
.controls {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.control-group {
display: flex;
align-items: center;
gap: 8px;
}
select, button {
background-color: #1a1a1a;
color: white;
border: 1px solid #333;
padding: 6px 10px;
border-radius: 3px;
font-size: 13px;
}
button:hover {
background-color: #2a2a2a;
cursor: pointer;
}
.status {
text-align: center;
padding: 8px;
border-radius: 4px;
margin-bottom: 15px;
font-weight: bold;
font-size: 0.9rem;
}
.status.connected {
background-color: #0a2a0a;
color: #00ff88;
border: 1px solid #00ff88;
}
.status.disconnected {
background-color: #2a0a0a;
color: #ff4444;
border: 1px solid #ff4444;
}
.dashboard-container {
display: grid;
grid-template-columns: 1fr 400px;
grid-template-rows: 1fr auto;
gap: 15px;
height: calc(100vh - 150px);
}
.chart-section {
display: flex;
flex-direction: column;
gap: 15px;
}
.price-chart-container {
background-color: #1a1a1a;
border-radius: 6px;
padding: 10px;
border: 1px solid #333;
flex: 1;
}
.svp-chart-container {
background-color: #1a1a1a;
border-radius: 6px;
padding: 10px;
border: 1px solid #333;
height: 200px;
}
.orderbook-section {
background-color: #1a1a1a;
border-radius: 6px;
padding: 10px;
border: 1px solid #333;
display: flex;
flex-direction: column;
}
.chart-title {
color: #00ff88;
font-size: 1rem;
font-weight: bold;
margin-bottom: 8px;
text-align: center;
}
.orderbook-header {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
padding: 8px 0;
border-bottom: 1px solid #333;
margin-bottom: 10px;
font-size: 0.85rem;
font-weight: bold;
color: #888;
}
.orderbook-content {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.asks-section, .bids-section {
flex: 1;
overflow-y: auto;
}
.mid-price-section {
padding: 10px 0;
text-align: center;
border-top: 1px solid #333;
border-bottom: 1px solid #333;
margin: 5px 0;
}
.mid-price {
font-size: 1.2rem;
font-weight: bold;
color: #00ff88;
}
.spread {
font-size: 0.8rem;
color: #888;
margin-top: 2px;
}
.orderbook-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 5px;
padding: 2px 0;
font-size: 0.8rem;
cursor: pointer;
}
.orderbook-row:hover {
background-color: #2a2a2a;
}
.ask-row {
color: #ff6b6b;
}
.bid-row {
color: #4ecdc4;
}
.price-cell {
text-align: right;
font-weight: bold;
}
.size-cell {
text-align: right;
}
.total-cell {
text-align: right;
color: #888;
}
.volume-bar {
position: absolute;
top: 0;
right: 0;
height: 100%;
opacity: 0.1;
z-index: -1;
}
.ask-bar {
background-color: #ff6b6b;
}
.bid-bar {
background-color: #4ecdc4;
}
.stats-container {
grid-column: span 2;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
}
.stat-card {
background-color: #1a1a1a;
border-radius: 6px;
padding: 12px;
border: 1px solid #333;
text-align: center;
}
.stat-label {
color: #888;
font-size: 0.8rem;
margin-bottom: 4px;
}
.stat-value {
color: #00ff88;
font-size: 1.2rem;
font-weight: bold;
}
.stat-sub {
color: #ccc;
font-size: 0.7rem;
margin-top: 3px;
}
#price-chart, #svp-chart {
height: 100%;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: #888;
font-size: 1rem;
}
</style>
</head>
<body>
<div class="header">
<h1>Consolidated Order Book Dashboard</h1>
<div class="subtitle">Hybrid WebSocket + REST API | Real-time + Deep Market Data</div>
</div>
<div class="controls">
<div class="control-group">
<label for="symbolSelect">Symbol:</label>
<select id="symbolSelect">
<option value="BTC/USDT">BTC/USDT</option>
<option value="ETH/USDT">ETH/USDT</option>
</select>
</div>
<div class="control-group">
<button onclick="toggleConnection()">Toggle Connection</button>
<button onclick="refreshData()">Refresh Data</button>
</div>
</div>
<div id="status" class="status disconnected">
Disconnected - Click Toggle Connection to start
</div>
<div class="dashboard-container">
<!-- Charts Section -->
<div class="chart-section">
<!-- Price Chart -->
<div class="price-chart-container">
<div class="chart-title">Price & Volume Analysis</div>
<div id="price-chart"></div>
</div>
<!-- Session Volume Profile -->
<div class="svp-chart-container">
<div class="chart-title">Session Volume Profile (SVP)</div>
<div id="svp-chart"></div>
</div>
</div>
<!-- Order Book Ladder -->
<div class="orderbook-section">
<div class="chart-title">Order Book Ladder</div>
<div class="orderbook-header">
<div>Price</div>
<div>Size</div>
<div>Total</div>
</div>
<div class="orderbook-content">
<!-- Asks (Sells) - Top half -->
<div class="asks-section" id="asks-section">
<div class="loading">Loading asks...</div>
</div>
<!-- Mid Price -->
<div class="mid-price-section">
<div class="mid-price" id="mid-price-display">$--</div>
<div class="spread" id="spread-display">Spread: -- bps</div>
</div>
<!-- Bids (Buys) - Bottom half -->
<div class="bids-section" id="bids-section">
<div class="loading">Loading bids...</div>
</div>
</div>
</div>
<!-- Statistics -->
<div class="stats-container">
<div class="stat-card">
<div class="stat-label">Total Liquidity</div>
<div class="stat-value" id="total-liquidity">--</div>
<div class="stat-sub" id="liquidity-breakdown">--</div>
</div>
<div class="stat-card">
<div class="stat-label">Book Depth</div>
<div class="stat-value" id="book-depth">--</div>
<div class="stat-sub" id="depth-breakdown">--</div>
</div>
<div class="stat-card">
<div class="stat-label">Imbalance</div>
<div class="stat-value" id="imbalance">--</div>
<div class="stat-sub">bid/ask ratio</div>
</div>
<div class="stat-card">
<div class="stat-label">Update Rate</div>
<div class="stat-value" id="update-rate">--</div>
<div class="stat-sub">updates/sec</div>
</div>
<div class="stat-card">
<div class="stat-label">Best Bid</div>
<div class="stat-value" id="best-bid">--</div>
<div class="stat-sub" id="bid-size">--</div>
</div>
<div class="stat-card">
<div class="stat-label">Best Ask</div>
<div class="stat-value" id="best-ask">--</div>
<div class="stat-sub" id="ask-size">--</div>
</div>
</div>
</div>
<script>
let ws = null;
let currentSymbol = 'BTC/USDT';
let isConnected = false;
let lastUpdateTime = 0;
let updateCount = 0;
let currentData = null;
// Initialize charts
function initializeCharts() {
// Price Chart Layout
const priceLayout = {
title: '',
xaxis: {
title: 'Time',
color: '#ffffff',
gridcolor: '#333',
showgrid: false
},
yaxis: {
title: 'Price',
color: '#ffffff',
gridcolor: '#333'
},
plot_bgcolor: '#0a0a0a',
paper_bgcolor: '#1a1a1a',
font: { color: '#ffffff' },
margin: { l: 60, r: 20, t: 20, b: 40 },
showlegend: false
};
// SVP Chart Layout
const svpLayout = {
title: '',
xaxis: {
title: 'Volume',
color: '#ffffff',
gridcolor: '#333'
},
yaxis: {
title: 'Price',
color: '#ffffff',
gridcolor: '#333'
},
plot_bgcolor: '#0a0a0a',
paper_bgcolor: '#1a1a1a',
font: { color: '#ffffff' },
margin: { l: 60, r: 20, t: 20, b: 40 }
};
// Initialize empty charts
Plotly.newPlot('price-chart', [], priceLayout, {responsive: true});
Plotly.newPlot('svp-chart', [], svpLayout, {responsive: true});
}
function connectWebSocket() {
if (ws) {
ws.close();
}
ws = new WebSocket(`ws://${window.location.host}/ws`);
ws.onopen = function() {
console.log('WebSocket connected');
isConnected = true;
updateStatus('Connected - Receiving real-time data', true);
};
ws.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
console.log('Received data:', data);
if (data.type === 'cob_update') {
handleCOBUpdate(data);
}
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
};
ws.onclose = function() {
console.log('WebSocket disconnected');
isConnected = false;
updateStatus('Disconnected - Click Toggle Connection to reconnect', false);
};
ws.onerror = function(error) {
console.error('WebSocket error:', error);
updateStatus('Connection Error - Check server status', false);
};
}
function handleCOBUpdate(data) {
// Handle nested data structure from API
if (data.data && data.data.data) {
currentData = data.data.data;
} else if (data.data) {
currentData = data.data;
} else {
currentData = data;
}
console.log('Processing COB data:', currentData);
const stats = currentData.stats || {};
// Update statistics
updateStatistics(stats);
// Update order book ladder
updateOrderBookLadder(currentData);
// Update SVP chart
updateSVPChart(currentData);
// Track update rate
updateCount++;
const now = Date.now();
if (now - lastUpdateTime >= 1000) {
document.getElementById('update-rate').textContent = updateCount;
updateCount = 0;
lastUpdateTime = now;
}
}
function updateOrderBookLadder(cobData) {
const bids = cobData.bids || [];
const asks = cobData.asks || [];
// Sort asks (lowest price first, closest to mid)
const sortedAsks = [...asks].sort((a, b) => a.price - b.price);
// Sort bids (highest price first, closest to mid)
const sortedBids = [...bids].sort((a, b) => b.price - a.price);
// Update asks section (top half)
const asksSection = document.getElementById('asks-section');
asksSection.innerHTML = '';
// Show asks in reverse order (highest first, then down to mid)
sortedAsks.reverse().forEach((ask, index) => {
const row = createOrderBookRow(ask, 'ask');
asksSection.appendChild(row);
});
// Update bids section (bottom half)
const bidsSection = document.getElementById('bids-section');
bidsSection.innerHTML = '';
// Show bids in normal order (highest first, then down)
sortedBids.forEach((bid, index) => {
const row = createOrderBookRow(bid, 'bid');
bidsSection.appendChild(row);
});
// Update mid price
const stats = cobData.stats || {};
document.getElementById('mid-price-display').textContent = `$${(stats.mid_price || 0).toFixed(2)}`;
document.getElementById('spread-display').textContent = `Spread: ${(stats.spread_bps || 0).toFixed(2)} bps`;
}
function createOrderBookRow(data, type) {
const row = document.createElement('div');
row.className = `orderbook-row ${type}-row`;
row.style.position = 'relative';
// Calculate total volume for bar width
const maxVolume = currentData ? Math.max(
...currentData.bids.map(b => b.volume),
...currentData.asks.map(a => a.volume)
) : 1;
const barWidth = (data.volume / maxVolume) * 100;
row.innerHTML = `
<div class="volume-bar ${type}-bar" style="width: ${barWidth}%"></div>
<div class="price-cell">$${data.price.toFixed(2)}</div>
<div class="size-cell">${(data.volume || 0).toFixed(4)}</div>
<div class="total-cell">$${((data.volume || 0) / 1000).toFixed(0)}K</div>
`;
return row;
}
function updateStatistics(stats) {
// Total Liquidity
const totalLiq = (stats.bid_liquidity + stats.ask_liquidity) || 0;
document.getElementById('total-liquidity').textContent = `$${(totalLiq / 1000).toFixed(0)}K`;
document.getElementById('liquidity-breakdown').textContent =
`Bid: $${(stats.bid_liquidity / 1000).toFixed(0)}K | Ask: $${(stats.ask_liquidity / 1000).toFixed(0)}K`;
// Order Book Depth
const bidCount = stats.bid_levels || 0;
const askCount = stats.ask_levels || 0;
document.getElementById('book-depth').textContent = `${bidCount + askCount}`;
document.getElementById('depth-breakdown').textContent = `${bidCount} bids | ${askCount} asks`;
// Imbalance
const imbalance = stats.imbalance || 0;
document.getElementById('imbalance').textContent = `${(imbalance * 100).toFixed(1)}%`;
// Best Bid/Ask
if (currentData && currentData.bids && currentData.bids.length > 0) {
const bestBid = Math.max(...currentData.bids.map(b => b.price));
const bestBidData = currentData.bids.find(b => b.price === bestBid);
document.getElementById('best-bid').textContent = `$${bestBid.toFixed(2)}`;
document.getElementById('bid-size').textContent = `${(bestBidData.volume || 0).toFixed(4)}`;
}
if (currentData && currentData.asks && currentData.asks.length > 0) {
const bestAsk = Math.min(...currentData.asks.map(a => a.price));
const bestAskData = currentData.asks.find(a => a.price === bestAsk);
document.getElementById('best-ask').textContent = `$${bestAsk.toFixed(2)}`;
document.getElementById('ask-size').textContent = `${(bestAskData.volume || 0).toFixed(4)}`;
}
}
function updateSVPChart(cobData) {
const svp = cobData.svp || [];
// Handle both array format and object format
let svpData = [];
if (Array.isArray(svp)) {
svpData = svp;
} else if (svp.data && Array.isArray(svp.data)) {
svpData = svp.data;
}
if (svpData.length === 0) {
console.log('No SVP data available');
return;
}
// Prepare SVP data
const buyTrace = {
x: svpData.map(d => -d.buy_volume),
y: svpData.map(d => d.price),
type: 'bar',
orientation: 'h',
name: 'Buy Volume',
marker: { color: 'rgba(78, 205, 196, 0.7)' },
hovertemplate: 'Price: $%{y}<br>Buy Volume: $%{customdata:,.0f}<extra></extra>',
customdata: svpData.map(d => d.buy_volume)
};
const sellTrace = {
x: svpData.map(d => d.sell_volume),
y: svpData.map(d => d.price),
type: 'bar',
orientation: 'h',
name: 'Sell Volume',
marker: { color: 'rgba(255, 107, 107, 0.7)' },
hovertemplate: 'Price: $%{y}<br>Sell Volume: $%{x:,.0f}<extra></extra>'
};
Plotly.redraw('svp-chart', [buyTrace, sellTrace]);
}
function updateStatus(message, connected) {
const statusEl = document.getElementById('status');
statusEl.textContent = message;
statusEl.className = `status ${connected ? 'connected' : 'disconnected'}`;
}
function toggleConnection() {
if (isConnected) {
if (ws) ws.close();
} else {
connectWebSocket();
}
}
function refreshData() {
if (isConnected) {
// Request fresh data from API
fetch(`/api/cob/${encodeURIComponent(currentSymbol)}`)
.then(response => response.json())
.then(data => {
console.log('Refreshed data:', data);
if (data.data) {
handleCOBUpdate({type: 'cob_update', data: data.data});
}
})
.catch(error => console.error('Error refreshing data:', error));
}
}
// Symbol change handler
document.getElementById('symbolSelect').addEventListener('change', function() {
currentSymbol = this.value;
console.log('Symbol changed to:', currentSymbol);
refreshData();
});
// Initialize dashboard
document.addEventListener('DOMContentLoaded', function() {
initializeCharts();
updateStatus('Connecting...', false);
// Auto-connect on load
setTimeout(() => {
connectWebSocket();
}, 500);
});
</script>
</body>
</html>