COB zoom slider

This commit is contained in:
Dobromir Popov
2025-06-18 18:03:23 +03:00
parent 7fbe3119cf
commit 8d80fb3bbe

View File

@ -309,6 +309,56 @@
.update-indicator.show { .update-indicator.show {
opacity: 1; 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;
}
</style> </style>
</head> </head>
<body> <body>
@ -322,6 +372,11 @@
<div class="controls"> <div class="controls">
<button onclick="toggleConnection()">Toggle Connection</button> <button onclick="toggleConnection()">Toggle Connection</button>
<button onclick="refreshData()">Refresh Data</button> <button onclick="refreshData()">Refresh Data</button>
<div class="resolution-control">
<label for="resolutionSlider">Resolution Multiplier: <span id="resolutionValue">1x</span></label>
<input type="range" id="resolutionSlider" min="1" max="10" value="1" oninput="updateResolution(this.value)">
<small>1x = $10 BTC / $1 ETH | 10x = $100 BTC / $10 ETH</small>
</div>
</div> </div>
<div id="status" class="status disconnected"> <div id="status" class="status disconnected">
@ -418,17 +473,24 @@
let totalUpdatesPerSec = 0; let totalUpdatesPerSec = 0;
let currentData = { 'BTC/USDT': null, 'ETH/USDT': null }; let currentData = { 'BTC/USDT': null, 'ETH/USDT': null };
// Resolution multiplier for bucket size adjustment
let resolutionMultiplier = 1;
// Imbalance tracking for aggregation // Imbalance tracking for aggregation
let imbalanceHistory = { let imbalanceHistory = {
'BTC/USDT': { 'BTC/USDT': {
values: [], values: [],
avg1s: 0, avg1s: 0,
avg5s: 0 avg5s: 0,
avg15s: 0,
avg30s: 0
}, },
'ETH/USDT': { 'ETH/USDT': {
values: [], values: [],
avg1s: 0, avg1s: 0,
avg5s: 0 avg5s: 0,
avg15s: 0,
avg30s: 0
} }
}; };
@ -540,15 +602,19 @@
// Add current imbalance with timestamp // Add current imbalance with timestamp
history.values.push({ value: imbalance, timestamp: now }); 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 cutoff5s = now - 5000;
const cutoff1s = now - 1000; 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 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) { if (values1s.length > 0) {
history.avg1s = values1s.reduce((sum, item) => sum + item.value, 0) / values1s.length; history.avg1s = values1s.reduce((sum, item) => sum + item.value, 0) / values1s.length;
@ -557,16 +623,26 @@
if (values5s.length > 0) { if (values5s.length > 0) {
history.avg5s = values5s.reduce((sum, item) => sum + item.value, 0) / values5s.length; 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) { function getBTCResolution(price) {
// BTC $10 buckets as configured in COB provider // BTC buckets: $10 base * multiplier (1x-10x = $10-$100)
return Math.round(price / 10) * 10; const bucketSize = 10 * resolutionMultiplier;
return Math.round(price / bucketSize) * bucketSize;
} }
function getETHResolution(price) { function getETHResolution(price) {
// ETH $1 buckets as configured in COB provider // ETH buckets: $1 base * multiplier (1x-10x = $1-$10)
return Math.round(price); const bucketSize = 1 * resolutionMultiplier;
return Math.round(price / bucketSize) * bucketSize;
} }
function updateOrderBook(prefix, cobData, resolutionFunc) { function updateOrderBook(prefix, cobData, resolutionFunc) {
@ -577,35 +653,72 @@
if (midPrice === 0) return; if (midPrice === 0) return;
// Use raw order book levels without any aggregation // Use wider price range for higher resolution multipliers to maintain depth
// Filter orders within ±2% of mid price for good depth with $10/$1 buckets const baseRange = 0.02; // 2% base range
const priceRange = midPrice * 0.02; // 2% range const expandedRange = baseRange * Math.max(1, resolutionMultiplier * 0.5); // Expand range for higher multipliers
const priceRange = midPrice * expandedRange;
const minPrice = midPrice - priceRange; const minPrice = midPrice - priceRange;
const maxPrice = midPrice + priceRange; const maxPrice = midPrice + priceRange;
// Convert bid/ask data to proper format without aggregation // Helper function to aggregate orders by resolution buckets
const filteredBids = bids function aggregateOrders(orders, isAsk = false) {
.map(bid => ({ const buckets = new Map();
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 orders.forEach(order => {
.map(ask => ({ const bucketPrice = resolutionFunc(order.price);
price: ask.price, if (!buckets.has(bucketPrice)) {
volume: ask.volume || 0, buckets.set(bucketPrice, {
value: (ask.volume || 0) * ask.price price: bucketPrice,
})) volume: 0,
.filter(ask => ask.price <= maxPrice && ask.price >= midPrice) value: 0
.sort((a, b) => a.price - b.price); // Lowest price first });
}
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 // Aggregate or use raw data based on resolution multiplier
const maxDisplayLevels = 50; // Increased from 30 for better depth let processedBids, processedAsks;
const displayBids = filteredBids.slice(0, maxDisplayLevels);
const displayAsks = filteredAsks.slice(0, maxDisplayLevels); 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 // Calculate maximum volume for bar scaling
const allVolumes = [...displayBids, ...displayAsks].map(order => order.volume); const allVolumes = [...displayBids, ...displayAsks].map(order => order.volume);
@ -667,7 +780,7 @@
}; };
updateStatistics(prefix, updatedStats); 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) { function createOrderBookRow(data, type, prefix) {
@ -727,14 +840,16 @@
const askCount = stats.ask_levels || 0; const askCount = stats.ask_levels || 0;
document.getElementById(`${prefix}-levels`).textContent = `${bidCount + askCount}`; 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 symbol = prefix === 'btc' ? 'BTC/USDT' : 'ETH/USDT';
const history = imbalanceHistory[symbol]; const history = imbalanceHistory[symbol];
const imbalance1s = (history.avg1s * 100).toFixed(1); const imbalance1s = (history.avg1s * 100).toFixed(1);
const imbalance5s = (history.avg5s * 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 = document.getElementById(`${prefix}-imbalance`).textContent =
`${imbalance1s}% (1s) | ${imbalance5s}% (5s)`; `${imbalance1s}% (1s) | ${imbalance5s}% (5s) | ${imbalance15s}% (15s) | ${imbalance30s}% (30s)`;
document.getElementById(`${prefix}-updates`).textContent = updateCounts[symbol]; 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 // Initialize dashboard
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
updateStatus('Connecting...', false); updateStatus('Connecting...', false);