COB zoom slider
This commit is contained in:
@ -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);
|
||||
|
Reference in New Issue
Block a user