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 @@
@@ -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);