From 8d80fb3bbe50758ce65c03cb4a2d08664654bfb0 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Wed, 18 Jun 2025 18:03:23 +0300 Subject: [PATCH] COB zoom slider --- web/cob_dashboard.html | 210 +++++++++++++++++++++++++++++++++-------- 1 file changed, 173 insertions(+), 37 deletions(-) diff --git a/web/cob_dashboard.html b/web/cob_dashboard.html index a885bb2..3457035 100644 --- a/web/cob_dashboard.html +++ b/web/cob_dashboard.html @@ -309,6 +309,56 @@ .update-indicator.show { opacity: 1; } + + .controls button:hover { + background-color: #555; + } + + .resolution-control { + display: flex; + flex-direction: column; + gap: 5px; + margin-left: 20px; + min-width: 300px; + } + + .resolution-control label { + color: #ccc; + font-size: 0.9rem; + font-weight: bold; + } + + .resolution-control input[type="range"] { + width: 100%; + height: 6px; + background: #333; + outline: none; + border-radius: 3px; + cursor: pointer; + } + + .resolution-control input[type="range"]::-webkit-slider-thumb { + appearance: none; + width: 16px; + height: 16px; + background: #00ff88; + border-radius: 50%; + cursor: pointer; + } + + .resolution-control input[type="range"]::-moz-range-thumb { + width: 16px; + height: 16px; + background: #00ff88; + border-radius: 50%; + cursor: pointer; + border: none; + } + + .resolution-control small { + color: #888; + font-size: 0.75rem; + } @@ -322,6 +372,11 @@
+
+ + + 1x = $10 BTC / $1 ETH | 10x = $100 BTC / $10 ETH +
@@ -418,17 +473,24 @@ let totalUpdatesPerSec = 0; let currentData = { 'BTC/USDT': null, 'ETH/USDT': null }; + // Resolution multiplier for bucket size adjustment + let resolutionMultiplier = 1; + // Imbalance tracking for aggregation let imbalanceHistory = { 'BTC/USDT': { values: [], avg1s: 0, - avg5s: 0 + avg5s: 0, + avg15s: 0, + avg30s: 0 }, 'ETH/USDT': { values: [], avg1s: 0, - avg5s: 0 + avg5s: 0, + avg15s: 0, + avg30s: 0 } }; @@ -540,15 +602,19 @@ // Add current imbalance with timestamp history.values.push({ value: imbalance, timestamp: now }); - // Remove old values (older than 5 seconds) + // Remove old values (older than 30 seconds) + const cutoff30s = now - 30000; + const cutoff15s = now - 15000; const cutoff5s = now - 5000; const cutoff1s = now - 1000; - history.values = history.values.filter(item => item.timestamp > cutoff5s); + history.values = history.values.filter(item => item.timestamp > cutoff30s); - // Calculate averages + // Calculate averages for different time windows const values1s = history.values.filter(item => item.timestamp > cutoff1s); - const values5s = history.values; + const values5s = history.values.filter(item => item.timestamp > cutoff5s); + const values15s = history.values.filter(item => item.timestamp > cutoff15s); + const values30s = history.values; if (values1s.length > 0) { history.avg1s = values1s.reduce((sum, item) => sum + item.value, 0) / values1s.length; @@ -557,16 +623,26 @@ if (values5s.length > 0) { history.avg5s = values5s.reduce((sum, item) => sum + item.value, 0) / values5s.length; } + + if (values15s.length > 0) { + history.avg15s = values15s.reduce((sum, item) => sum + item.value, 0) / values15s.length; + } + + if (values30s.length > 0) { + history.avg30s = values30s.reduce((sum, item) => sum + item.value, 0) / values30s.length; + } } function getBTCResolution(price) { - // BTC $10 buckets as configured in COB provider - return Math.round(price / 10) * 10; + // BTC buckets: $10 base * multiplier (1x-10x = $10-$100) + const bucketSize = 10 * resolutionMultiplier; + return Math.round(price / bucketSize) * bucketSize; } function getETHResolution(price) { - // ETH $1 buckets as configured in COB provider - return Math.round(price); + // ETH buckets: $1 base * multiplier (1x-10x = $1-$10) + const bucketSize = 1 * resolutionMultiplier; + return Math.round(price / bucketSize) * bucketSize; } function updateOrderBook(prefix, cobData, resolutionFunc) { @@ -577,35 +653,72 @@ 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 + // Use wider price range for higher resolution multipliers to maintain depth + const baseRange = 0.02; // 2% base range + const expandedRange = baseRange * Math.max(1, resolutionMultiplier * 0.5); // Expand range for higher multipliers + const priceRange = midPrice * expandedRange; 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 + // Helper function to aggregate orders by resolution buckets + function aggregateOrders(orders, isAsk = false) { + const buckets = new Map(); - 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 + orders.forEach(order => { + const bucketPrice = resolutionFunc(order.price); + if (!buckets.has(bucketPrice)) { + buckets.set(bucketPrice, { + price: bucketPrice, + volume: 0, + value: 0 + }); + } + const bucket = buckets.get(bucketPrice); + bucket.volume += order.volume || 0; + bucket.value += (order.volume || 0) * order.price; + }); + + return Array.from(buckets.values()) + .filter(bucket => bucket.price >= minPrice && bucket.price <= maxPrice) + .filter(bucket => isAsk ? bucket.price >= midPrice : bucket.price <= midPrice); + } - // 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); + // Aggregate or use raw data based on resolution multiplier + let processedBids, processedAsks; + + if (resolutionMultiplier > 1) { + // Aggregate by resolution buckets with expanded range + processedBids = aggregateOrders(bids, false) + .sort((a, b) => b.price - a.price); // Highest price first + processedAsks = aggregateOrders(asks, true) + .sort((a, b) => a.price - b.price); // Lowest price first + + console.log(`${prefix.toUpperCase()} aggregated: ${processedBids.length} bid buckets, ${processedAsks.length} ask buckets (${resolutionMultiplier}x, ±${(expandedRange*100).toFixed(1)}%)`); + } else { + // Use raw data without aggregation + processedBids = 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); + + processedAsks = 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); + } + + // Increase display levels for aggregated data to show more depth + const maxDisplayLevels = resolutionMultiplier > 1 ? 75 : 50; // More levels for aggregated data + const displayBids = processedBids.slice(0, maxDisplayLevels); + const displayAsks = processedAsks.slice(0, maxDisplayLevels); // Calculate maximum volume for bar scaling const allVolumes = [...displayBids, ...displayAsks].map(order => order.volume); @@ -667,7 +780,7 @@ }; updateStatistics(prefix, updatedStats); - console.log(`${prefix.toUpperCase()}: Displayed ${displayBids.length} bids, ${displayAsks.length} asks from ${bids.length}/${asks.length} total (±2% range)`); + console.log(`${prefix.toUpperCase()}: Displayed ${displayBids.length} bids, ${displayAsks.length} asks from ${bids.length}/${asks.length} total (±${(expandedRange*100).toFixed(1)}%)`); } function createOrderBookRow(data, type, prefix) { @@ -727,14 +840,16 @@ const askCount = stats.ask_levels || 0; document.getElementById(`${prefix}-levels`).textContent = `${bidCount + askCount}`; - // Show aggregated imbalance (1s and 5s averages) + // Show aggregated imbalance (all time windows) 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); + const imbalance15s = (history.avg15s * 100).toFixed(1); + const imbalance30s = (history.avg30s * 100).toFixed(1); document.getElementById(`${prefix}-imbalance`).textContent = - `${imbalance1s}% (1s) | ${imbalance5s}% (5s)`; + `${imbalance1s}% (1s) | ${imbalance5s}% (5s) | ${imbalance15s}% (15s) | ${imbalance30s}% (30s)`; document.getElementById(`${prefix}-updates`).textContent = updateCounts[symbol]; } @@ -787,6 +902,27 @@ } } + function updateResolution(value) { + resolutionMultiplier = parseInt(value); + document.getElementById('resolutionValue').textContent = `${resolutionMultiplier}x`; + + // Update subtitle to show current bucket sizes + const btcBucket = 10 * resolutionMultiplier; + const ethBucket = 1 * resolutionMultiplier; + document.querySelector('.subtitle').textContent = + `Real-time COB Data | BTC ($${btcBucket} buckets) | ETH ($${ethBucket} buckets)`; + + // Refresh current data with new resolution + if (currentData['BTC/USDT']) { + updateOrderBook('btc', currentData['BTC/USDT'], getBTCResolution); + } + if (currentData['ETH/USDT']) { + updateOrderBook('eth', currentData['ETH/USDT'], getETHResolution); + } + + console.log(`Resolution updated to ${resolutionMultiplier}x (BTC: $${btcBucket}, ETH: $${ethBucket})`); + } + // Initialize dashboard document.addEventListener('DOMContentLoaded', function() { updateStatus('Connecting...', false);