Files
gogo2/web/cob_dashboard.html
Dobromir Popov 7fbe3119cf BOM WORKING!!!!
2025-06-18 17:50:24 +03:00

814 lines
28 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>COB Order Books - BTC & ETH</title>
<style>
body {
font-family: 'Courier New', monospace;
margin: 0;
padding: 15px;
background-color: #0a0a0a;
color: #ffffff;
overflow-x: auto;
}
.header {
text-align: center;
margin-bottom: 20px;
}
.header h1 {
color: #00ff88;
margin: 0;
font-size: 1.8rem;
}
.header .subtitle {
color: #888;
font-size: 0.9rem;
margin-top: 5px;
}
.status {
text-align: center;
padding: 8px;
border-radius: 4px;
margin-bottom: 20px;
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;
}
.controls {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
button {
background-color: #1a1a1a;
color: white;
border: 1px solid #333;
padding: 8px 15px;
border-radius: 3px;
font-size: 13px;
cursor: pointer;
}
button:hover {
background-color: #2a2a2a;
}
.orderbooks-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
height: calc(100vh - 180px);
}
.orderbook-panel {
background-color: #1a1a1a;
border-radius: 6px;
padding: 15px;
border: 1px solid #333;
display: flex;
flex-direction: column;
}
.orderbook-title {
color: #00ff88;
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 10px;
text-align: center;
border-bottom: 1px solid #333;
padding-bottom: 10px;
}
.price-resolution {
color: #888;
font-size: 0.8rem;
text-align: center;
margin-bottom: 15px;
}
.orderbook-header {
display: grid;
grid-template-columns: 80px 1fr 1fr 1fr;
gap: 8px;
padding: 8px 0;
border-bottom: 1px solid #333;
margin-bottom: 10px;
font-size: 0.85rem;
font-weight: bold;
color: #888;
text-align: center;
}
.orderbook-content {
flex: 1;
overflow-y: auto;
}
.unified-orderbook {
display: flex;
flex-direction: column;
}
.mid-price-section {
padding: 10px 0;
text-align: center;
border-top: 2px solid #00ff88;
border-bottom: 2px solid #00ff88;
margin: 5px 0;
background-color: #0f0f0f;
position: sticky;
top: 0;
z-index: 100;
}
.mid-price {
font-size: 1.4rem;
font-weight: bold;
color: #00ff88;
}
.spread {
font-size: 0.9rem;
color: #888;
margin-top: 4px;
}
.orderbook-row {
display: grid;
grid-template-columns: 60px 100px 1fr 80px;
gap: 4px;
padding: 0;
margin: 0;
font-size: 0.75rem;
text-align: center;
border-bottom: 1px solid #1a1a1a;
transition: background-color 0.2s;
position: relative;
min-height: 18px;
align-items: center;
overflow: hidden;
}
.orderbook-row:hover {
background-color: rgba(42, 42, 42, 0.3);
z-index: 10;
}
.ask-row {
color: #ff6b6b;
}
.bid-row {
color: #4ecdc4;
}
.volume-bar {
position: absolute;
top: 0;
left: 0;
height: 100%;
opacity: 1;
z-index: 1;
transition: none;
border: none;
margin: 0;
padding: 0;
}
.ask-bar {
background: linear-gradient(90deg, rgba(255, 107, 107, 0.6) 0%, rgba(255, 107, 107, 1.0) 100%);
border-right: 1px solid #ff6b6b;
}
.bid-bar {
background: linear-gradient(90deg, rgba(78, 205, 196, 0.6) 0%, rgba(78, 205, 196, 1.0) 100%);
border-right: 1px solid #4ecdc4;
}
.price-label {
display: none;
}
.price-label.show {
display: block;
}
.side-cell, .price-cell, .size-cell, .total-cell {
position: relative;
z-index: 2;
background: transparent;
padding: 2px 4px;
margin: 0;
}
.side-cell {
font-weight: bold;
font-size: 0.7rem;
text-align: center;
}
.ask-side {
color: #ff6b6b;
text-shadow: 0 0 2px rgba(0,0,0,0.8);
}
.bid-side {
color: #4ecdc4;
text-shadow: 0 0 2px rgba(0,0,0,0.8);
}
.price-cell {
font-weight: bold;
text-shadow: 0 0 2px rgba(0,0,0,0.8);
}
.size-cell {
color: #ccc;
text-shadow: 0 0 2px rgba(0,0,0,0.8);
}
.total-cell {
color: #888;
font-size: 0.8rem;
text-shadow: 0 0 2px rgba(0,0,0,0.8);
}
.stats-summary {
margin-top: 15px;
padding: 10px;
background-color: #0f0f0f;
border-radius: 4px;
border: 1px solid #333;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
font-size: 0.8rem;
}
.stat-item {
display: flex;
justify-content: space-between;
}
.stat-label {
color: #888;
}
.stat-value {
color: #00ff88;
font-weight: bold;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100px;
color: #888;
font-size: 1rem;
}
/* Update indicator */
.update-indicator {
position: fixed;
top: 10px;
right: 10px;
padding: 5px 10px;
background-color: #00ff88;
color: #000;
border-radius: 3px;
font-size: 0.8rem;
opacity: 0;
transition: opacity 0.3s;
}
.update-indicator.show {
opacity: 1;
}
</style>
</head>
<body>
<div class="update-indicator" id="updateIndicator">Updates/sec: 0</div>
<div class="header">
<h1>Consolidated Order Books</h1>
<div class="subtitle">Real-time COB Data | BTC ($10 buckets) | ETH ($1 buckets)</div>
</div>
<div class="controls">
<button onclick="toggleConnection()">Toggle Connection</button>
<button onclick="refreshData()">Refresh Data</button>
</div>
<div id="status" class="status disconnected">
Disconnected - Click Toggle Connection to start
</div>
<div class="orderbooks-container">
<!-- BTC Order Book -->
<div class="orderbook-panel">
<div class="orderbook-title">BTC/USDT</div>
<div class="price-resolution">Resolution: $10 buckets</div>
<div class="orderbook-header">
<div>Side</div>
<div>Price</div>
<div>Size (BTC)</div>
<div>Total ($)</div>
</div>
<div class="orderbook-content">
<div class="unified-orderbook" id="btc-unified-orderbook">
<div class="loading">Loading BTC order book...</div>
</div>
</div>
<div class="stats-summary">
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Liquidity:</span>
<span class="stat-value" id="btc-liquidity">--</span>
</div>
<div class="stat-item">
<span class="stat-label">Levels:</span>
<span class="stat-value" id="btc-levels">--</span>
</div>
<div class="stat-item">
<span class="stat-label">Imbalance (1s/5s):</span>
<span class="stat-value" id="btc-imbalance">--</span>
</div>
<div class="stat-item">
<span class="stat-label">Updates:</span>
<span class="stat-value" id="btc-updates">--</span>
</div>
</div>
</div>
</div>
<!-- ETH Order Book -->
<div class="orderbook-panel">
<div class="orderbook-title">ETH/USDT</div>
<div class="price-resolution">Resolution: $1 buckets</div>
<div class="orderbook-header">
<div>Side</div>
<div>Price</div>
<div>Size (ETH)</div>
<div>Total ($)</div>
</div>
<div class="orderbook-content">
<div class="unified-orderbook" id="eth-unified-orderbook">
<div class="loading">Loading ETH order book...</div>
</div>
</div>
<div class="stats-summary">
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Liquidity:</span>
<span class="stat-value" id="eth-liquidity">--</span>
</div>
<div class="stat-item">
<span class="stat-label">Levels:</span>
<span class="stat-value" id="eth-levels">--</span>
</div>
<div class="stat-item">
<span class="stat-label">Imbalance:</span>
<span class="stat-value" id="eth-imbalance">--</span>
</div>
<div class="stat-item">
<span class="stat-label">Updates:</span>
<span class="stat-value" id="eth-updates">--</span>
</div>
</div>
</div>
</div>
</div>
<script>
let ws = null;
let isConnected = false;
let updateCounts = { 'BTC/USDT': 0, 'ETH/USDT': 0 };
let lastUpdateTime = Date.now();
let totalUpdatesPerSec = 0;
let currentData = { 'BTC/USDT': null, 'ETH/USDT': null };
// Imbalance tracking for aggregation
let imbalanceHistory = {
'BTC/USDT': {
values: [],
avg1s: 0,
avg5s: 0
},
'ETH/USDT': {
values: [],
avg1s: 0,
avg5s: 0
}
};
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 COB data', true);
};
ws.onmessage = function(event) {
try {
const data = JSON.parse(event.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) {
const symbol = data.symbol;
// Handle nested data structure from API
let cobData;
if (data.data && data.data.data) {
cobData = data.data.data;
} else if (data.data) {
cobData = data.data;
} else {
cobData = data;
}
// Debug logging to understand data structure
console.log(`${symbol} COB Update:`, {
bidsCount: (cobData.bids || []).length,
asksCount: (cobData.asks || []).length,
sampleBid: (cobData.bids || [])[0],
sampleAsk: (cobData.asks || [])[0],
stats: cobData.stats
});
// Check if WebSocket data has insufficient depth, fetch REST data
const bids = cobData.bids || [];
const asks = cobData.asks || [];
if (bids.length <= 1 && asks.length <= 1) {
console.log(`Insufficient WS depth for ${symbol}, fetching REST data...`);
fetchRESTData(symbol);
return;
}
currentData[symbol] = cobData;
// Track imbalance for aggregation
const stats = cobData.stats || {};
if (stats.imbalance !== undefined) {
trackImbalance(symbol, stats.imbalance);
}
// Update the appropriate order book
if (symbol === 'BTC/USDT') {
updateOrderBook('btc', cobData, getBTCResolution);
} else if (symbol === 'ETH/USDT') {
updateOrderBook('eth', cobData, getETHResolution);
}
// Track update rate
updateCounts[symbol]++;
updateUpdateRate();
}
function fetchRESTData(symbol) {
fetch(`/api/cob/${encodeURIComponent(symbol)}`)
.then(response => response.json())
.then(data => {
if (data.data) {
console.log(`REST fallback data for ${symbol}:`, data.data);
handleCOBUpdate({symbol: symbol, data: data.data});
}
})
.catch(error => console.error(`Error fetching REST data for ${symbol}:`, error));
}
function trackImbalance(symbol, imbalance) {
const now = Date.now();
const history = imbalanceHistory[symbol];
// Add current imbalance with timestamp
history.values.push({ value: imbalance, timestamp: now });
// Remove old values (older than 5 seconds)
const cutoff5s = now - 5000;
const cutoff1s = now - 1000;
history.values = history.values.filter(item => item.timestamp > cutoff5s);
// Calculate averages
const values1s = history.values.filter(item => item.timestamp > cutoff1s);
const values5s = history.values;
if (values1s.length > 0) {
history.avg1s = values1s.reduce((sum, item) => sum + item.value, 0) / values1s.length;
}
if (values5s.length > 0) {
history.avg5s = values5s.reduce((sum, item) => sum + item.value, 0) / values5s.length;
}
}
function getBTCResolution(price) {
// BTC $10 buckets as configured in COB provider
return Math.round(price / 10) * 10;
}
function getETHResolution(price) {
// ETH $1 buckets as configured in COB provider
return Math.round(price);
}
function updateOrderBook(prefix, cobData, resolutionFunc) {
const bids = cobData.bids || [];
const asks = cobData.asks || [];
const stats = cobData.stats || {};
const midPrice = stats.mid_price || 0;
if (midPrice === 0) return;
// Use raw order book levels without any aggregation
// Filter orders within ±2% of mid price for good depth with $10/$1 buckets
const priceRange = midPrice * 0.02; // 2% range
const minPrice = midPrice - priceRange;
const maxPrice = midPrice + priceRange;
// Convert bid/ask data to proper format without aggregation
const filteredBids = bids
.map(bid => ({
price: bid.price,
volume: bid.volume || 0,
value: (bid.volume || 0) * bid.price
}))
.filter(bid => bid.price >= minPrice && bid.price <= midPrice)
.sort((a, b) => b.price - a.price); // Highest price first
const filteredAsks = asks
.map(ask => ({
price: ask.price,
volume: ask.volume || 0,
value: (ask.volume || 0) * ask.price
}))
.filter(ask => ask.price <= maxPrice && ask.price >= midPrice)
.sort((a, b) => a.price - b.price); // Lowest price first
// Limit to reasonable display count but show good depth
const maxDisplayLevels = 50; // Increased from 30 for better depth
const displayBids = filteredBids.slice(0, maxDisplayLevels);
const displayAsks = filteredAsks.slice(0, maxDisplayLevels);
// Calculate maximum volume for bar scaling
const allVolumes = [...displayBids, ...displayAsks].map(order => order.volume);
const maxVolume = Math.max(...allVolumes, 1);
// Combine all orders and sort by price (highest to lowest)
const allOrders = [];
// Add asks first (highest ask prices at top)
displayAsks.reverse().forEach((ask, index) => {
allOrders.push({
...ask,
side: 'ASK',
showPrice: index % 10 === 0, // Show every 10th price for readability
volumePercent: (ask.volume / maxVolume) * 100
});
});
// Add mid-price marker
allOrders.push({
price: midPrice,
volume: 0,
value: 0,
side: 'MID',
spread: stats.spread_bps || 0,
showPrice: true,
volumePercent: 0
});
// Add bids (highest bid prices below mid)
displayBids.forEach((bid, index) => {
allOrders.push({
...bid,
side: 'BID',
showPrice: index % 10 === 0, // Show every 10th price for readability
volumePercent: (bid.volume / maxVolume) * 100
});
});
// Update unified order book
const unifiedOrderbook = document.getElementById(`${prefix}-unified-orderbook`);
unifiedOrderbook.innerHTML = '';
allOrders.forEach(order => {
if (order.side === 'MID') {
const midRow = createMidPriceRow(order, prefix);
unifiedOrderbook.appendChild(midRow);
} else {
const row = createOrderBookRow(order, order.side.toLowerCase(), prefix);
unifiedOrderbook.appendChild(row);
}
});
// Update statistics with actual counts
const updatedStats = {
...stats,
bid_levels: displayBids.length,
ask_levels: displayAsks.length
};
updateStatistics(prefix, updatedStats);
console.log(`${prefix.toUpperCase()}: Displayed ${displayBids.length} bids, ${displayAsks.length} asks from ${bids.length}/${asks.length} total (±2% range)`);
}
function createOrderBookRow(data, type, prefix) {
const row = document.createElement('div');
row.className = `orderbook-row ${type}-row`;
const isETH = prefix === 'eth';
const volumeDecimals = isETH ? 3 : 6;
// Only show price label every 5th row or if explicitly flagged
const showPrice = data.showPrice !== false;
const priceDisplay = showPrice ?
`$${data.price.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}` :
'';
const sideText = data.side || (type === 'ask' ? 'ASK' : 'BID');
const sideClass = type === 'ask' ? 'ask-side' : 'bid-side';
// Create volume bar
const volumePercent = data.volumePercent || 0;
const barClass = type === 'ask' ? 'ask-bar' : 'bid-bar';
row.innerHTML = `
<div class="volume-bar ${barClass}" style="width: ${volumePercent}%"></div>
<div class="side-cell ${sideClass}">${sideText}</div>
<div class="price-cell">${priceDisplay}</div>
<div class="size-cell">${data.volume.toFixed(volumeDecimals)}</div>
<div class="total-cell">$${(data.value / 1000).toFixed(0)}K</div>
`;
return row;
}
function createMidPriceRow(data, prefix) {
const row = document.createElement('div');
row.className = 'mid-price-section';
const priceFormatted = data.price.toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
row.innerHTML = `
<div class="mid-price">$${priceFormatted}</div>
<div class="spread">Spread: ${data.spread.toFixed(2)} bps</div>
`;
return row;
}
function updateStatistics(prefix, stats) {
const totalLiq = (stats.bid_liquidity + stats.ask_liquidity) || 0;
document.getElementById(`${prefix}-liquidity`).textContent =
`$${(totalLiq / 1000).toFixed(0)}K`;
const bidCount = stats.bid_levels || 0;
const askCount = stats.ask_levels || 0;
document.getElementById(`${prefix}-levels`).textContent = `${bidCount + askCount}`;
// Show aggregated imbalance (1s and 5s averages)
const symbol = prefix === 'btc' ? 'BTC/USDT' : 'ETH/USDT';
const history = imbalanceHistory[symbol];
const imbalance1s = (history.avg1s * 100).toFixed(1);
const imbalance5s = (history.avg5s * 100).toFixed(1);
document.getElementById(`${prefix}-imbalance`).textContent =
`${imbalance1s}% (1s) | ${imbalance5s}% (5s)`;
document.getElementById(`${prefix}-updates`).textContent = updateCounts[symbol];
}
function updateUpdateRate() {
const now = Date.now();
if (now - lastUpdateTime >= 1000) {
totalUpdatesPerSec = updateCounts['BTC/USDT'] + updateCounts['ETH/USDT'];
const indicator = document.getElementById('updateIndicator');
indicator.textContent = `Updates/sec: ${totalUpdatesPerSec}`;
indicator.classList.add('show');
setTimeout(() => {
indicator.classList.remove('show');
}, 1000);
updateCounts['BTC/USDT'] = 0;
updateCounts['ETH/USDT'] = 0;
lastUpdateTime = now;
}
}
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) {
['BTC/USDT', 'ETH/USDT'].forEach(symbol => {
fetch(`/api/cob/${encodeURIComponent(symbol)}`)
.then(response => response.json())
.then(data => {
if (data.data) {
handleCOBUpdate({symbol: symbol, data: data.data});
}
})
.catch(error => console.error(`Error refreshing ${symbol} data:`, error));
});
}
}
// Initialize dashboard
document.addEventListener('DOMContentLoaded', function() {
updateStatus('Connecting...', false);
// Auto-connect on load
setTimeout(() => {
connectWebSocket();
}, 500);
// Periodically fetch REST data to ensure depth
setInterval(() => {
if (isConnected) {
['BTC/USDT', 'ETH/USDT'].forEach(symbol => {
const data = currentData[symbol];
if (!data || (data.bids && data.asks && data.bids.length <= 2 && data.asks.length <= 2)) {
console.log(`Fetching REST data for ${symbol} - insufficient depth`);
fetchRESTData(symbol);
}
});
}
}, 3000); // Check every 3 seconds
});
</script>
</body>
</html>