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 {
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>
</head>
<body>
@ -322,6 +372,11 @@
<div class="controls">
<button onclick="toggleConnection()">Toggle Connection</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 id="status" class="status disconnected">
@ -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);