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