small ui fixes
This commit is contained in:
@ -154,6 +154,35 @@
|
|||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mini-chart-container {
|
||||||
|
margin: 8px 0;
|
||||||
|
padding: 6px;
|
||||||
|
background-color: #0a0a0a;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
position: relative;
|
||||||
|
background-color: #111;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-chart canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #888;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.orderbook-row {
|
.orderbook-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 60px 100px 1fr 80px;
|
grid-template-columns: 60px 100px 1fr 80px;
|
||||||
@ -386,8 +415,8 @@
|
|||||||
<div class="orderbooks-container">
|
<div class="orderbooks-container">
|
||||||
<!-- BTC Order Book -->
|
<!-- BTC Order Book -->
|
||||||
<div class="orderbook-panel">
|
<div class="orderbook-panel">
|
||||||
<div class="orderbook-title">BTC/USDT</div>
|
<div class="orderbook-title" id="btc-title">BTC/USDT - $--</div>
|
||||||
<div class="price-resolution">Resolution: $10 buckets</div>
|
<div class="price-resolution" id="btc-resolution">Resolution: $10 buckets</div>
|
||||||
|
|
||||||
<div class="orderbook-header">
|
<div class="orderbook-header">
|
||||||
<div>Side</div>
|
<div>Side</div>
|
||||||
@ -426,8 +455,8 @@
|
|||||||
|
|
||||||
<!-- ETH Order Book -->
|
<!-- ETH Order Book -->
|
||||||
<div class="orderbook-panel">
|
<div class="orderbook-panel">
|
||||||
<div class="orderbook-title">ETH/USDT</div>
|
<div class="orderbook-title" id="eth-title">ETH/USDT - $--</div>
|
||||||
<div class="price-resolution">Resolution: $1 buckets</div>
|
<div class="price-resolution" id="eth-resolution">Resolution: $1 buckets</div>
|
||||||
|
|
||||||
<div class="orderbook-header">
|
<div class="orderbook-header">
|
||||||
<div>Side</div>
|
<div>Side</div>
|
||||||
@ -493,6 +522,12 @@
|
|||||||
avg30s: 0
|
avg30s: 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// OHLCV data storage for mini charts
|
||||||
|
let ohlcvData = {
|
||||||
|
'BTC/USDT': [],
|
||||||
|
'ETH/USDT': []
|
||||||
|
};
|
||||||
|
|
||||||
function connectWebSocket() {
|
function connectWebSocket() {
|
||||||
if (ws) {
|
if (ws) {
|
||||||
@ -550,7 +585,10 @@
|
|||||||
asksCount: (cobData.asks || []).length,
|
asksCount: (cobData.asks || []).length,
|
||||||
sampleBid: (cobData.bids || [])[0],
|
sampleBid: (cobData.bids || [])[0],
|
||||||
sampleAsk: (cobData.asks || [])[0],
|
sampleAsk: (cobData.asks || [])[0],
|
||||||
stats: cobData.stats
|
stats: cobData.stats,
|
||||||
|
hasOHLCV: !!cobData.ohlcv,
|
||||||
|
ohlcvCount: cobData.ohlcv ? cobData.ohlcv.length : 0,
|
||||||
|
sampleOHLCV: cobData.ohlcv ? cobData.ohlcv[0] : null
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if WebSocket data has insufficient depth, fetch REST data
|
// Check if WebSocket data has insufficient depth, fetch REST data
|
||||||
@ -565,6 +603,20 @@
|
|||||||
|
|
||||||
currentData[symbol] = cobData;
|
currentData[symbol] = cobData;
|
||||||
|
|
||||||
|
// Process OHLCV data if available
|
||||||
|
if (cobData.ohlcv && Array.isArray(cobData.ohlcv)) {
|
||||||
|
ohlcvData[symbol] = cobData.ohlcv;
|
||||||
|
console.log(`${symbol} OHLCV data received:`, cobData.ohlcv.length, 'candles');
|
||||||
|
|
||||||
|
// Update mini chart after order book update
|
||||||
|
setTimeout(() => {
|
||||||
|
const prefix = symbol === 'BTC/USDT' ? 'btc' : 'eth';
|
||||||
|
drawMiniChart(prefix, cobData.ohlcv);
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
console.log(`${symbol}: No OHLCV data in update`);
|
||||||
|
}
|
||||||
|
|
||||||
// Track imbalance for aggregation
|
// Track imbalance for aggregation
|
||||||
const stats = cobData.stats || {};
|
const stats = cobData.stats || {};
|
||||||
if (stats.imbalance !== undefined) {
|
if (stats.imbalance !== undefined) {
|
||||||
@ -645,13 +697,21 @@
|
|||||||
return Math.round(price / bucketSize) * bucketSize;
|
return Math.round(price / bucketSize) * bucketSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateOrderBook(prefix, cobData, resolutionFunc) {
|
function updateOrderBook(prefix, cobData, resolutionFunc) {
|
||||||
const bids = cobData.bids || [];
|
const bids = cobData.bids || [];
|
||||||
const asks = cobData.asks || [];
|
const asks = cobData.asks || [];
|
||||||
const stats = cobData.stats || {};
|
const stats = cobData.stats || {};
|
||||||
const midPrice = stats.mid_price || 0;
|
const midPrice = stats.mid_price || 0;
|
||||||
|
|
||||||
if (midPrice === 0) return;
|
if (midPrice === 0) return;
|
||||||
|
|
||||||
|
// Update title with current price
|
||||||
|
const symbol = prefix === 'btc' ? 'BTC/USDT' : 'ETH/USDT';
|
||||||
|
const priceFormatted = midPrice.toLocaleString(undefined, {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
});
|
||||||
|
document.getElementById(`${prefix}-title`).textContent = `${symbol} - $${priceFormatted}`;
|
||||||
|
|
||||||
// Use wider price range for higher resolution multipliers to maintain depth
|
// Use wider price range for higher resolution multipliers to maintain depth
|
||||||
const baseRange = 0.02; // 2% base range
|
const baseRange = 0.02; // 2% base range
|
||||||
@ -856,9 +916,13 @@
|
|||||||
maximumFractionDigits: 2
|
maximumFractionDigits: 2
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const spreadText = data.spread ? `${data.spread.toFixed(1)} bps` : '--';
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<div class="mid-price">$${priceFormatted}</div>
|
<div class="chart-title">1s OHLCV (5min)</div>
|
||||||
<div class="spread">Spread: ${data.spread.toFixed(2)} bps</div>
|
<div class="mini-chart">
|
||||||
|
<canvas id="${prefix}-mini-chart" width="200" height="60"></canvas>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return row;
|
return row;
|
||||||
@ -945,6 +1009,10 @@
|
|||||||
document.querySelector('.subtitle').textContent =
|
document.querySelector('.subtitle').textContent =
|
||||||
`Real-time COB Data | BTC ($${btcBucket} buckets) | ETH ($${ethBucket} buckets)`;
|
`Real-time COB Data | BTC ($${btcBucket} buckets) | ETH ($${ethBucket} buckets)`;
|
||||||
|
|
||||||
|
// Update resolution text in panels
|
||||||
|
document.getElementById('btc-resolution').textContent = `Resolution: $${btcBucket} buckets`;
|
||||||
|
document.getElementById('eth-resolution').textContent = `Resolution: $${ethBucket} buckets`;
|
||||||
|
|
||||||
// Refresh current data with new resolution
|
// Refresh current data with new resolution
|
||||||
if (currentData['BTC/USDT']) {
|
if (currentData['BTC/USDT']) {
|
||||||
updateOrderBook('btc', currentData['BTC/USDT'], getBTCResolution);
|
updateOrderBook('btc', currentData['BTC/USDT'], getBTCResolution);
|
||||||
@ -956,10 +1024,136 @@
|
|||||||
console.log(`Resolution updated to ${resolutionMultiplier}x (BTC: $${btcBucket}, ETH: $${ethBucket})`);
|
console.log(`Resolution updated to ${resolutionMultiplier}x (BTC: $${btcBucket}, ETH: $${ethBucket})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function drawMiniChart(prefix, ohlcvArray) {
|
||||||
|
try {
|
||||||
|
const canvas = document.getElementById(`${prefix}-mini-chart`);
|
||||||
|
if (!canvas) {
|
||||||
|
console.log(`Canvas not found for ${prefix}-mini-chart`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const width = canvas.width;
|
||||||
|
const height = canvas.height;
|
||||||
|
|
||||||
|
console.log(`Drawing ${prefix} chart with ${ohlcvArray ? ohlcvArray.length : 0} candles`);
|
||||||
|
|
||||||
|
// Clear canvas
|
||||||
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
|
if (!ohlcvArray || ohlcvArray.length === 0) {
|
||||||
|
// Draw "No Data" message
|
||||||
|
ctx.fillStyle = '#555';
|
||||||
|
ctx.font = '12px Courier New';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText('No Data', width / 2, height / 2);
|
||||||
|
console.log(`${prefix}: No OHLCV data to draw`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get price range for scaling
|
||||||
|
const prices = [];
|
||||||
|
ohlcvArray.forEach(candle => {
|
||||||
|
prices.push(candle.high, candle.low);
|
||||||
|
});
|
||||||
|
|
||||||
|
const minPrice = Math.min(...prices);
|
||||||
|
const maxPrice = Math.max(...prices);
|
||||||
|
const priceRange = maxPrice - minPrice;
|
||||||
|
|
||||||
|
if (priceRange === 0) return;
|
||||||
|
|
||||||
|
// Calculate candle width and spacing
|
||||||
|
const candleWidth = Math.max(1, Math.floor(width / ohlcvArray.length) - 1);
|
||||||
|
const candleSpacing = width / ohlcvArray.length;
|
||||||
|
|
||||||
|
// Draw candlesticks
|
||||||
|
ohlcvArray.forEach((candle, index) => {
|
||||||
|
const x = index * candleSpacing + candleSpacing / 2;
|
||||||
|
|
||||||
|
// Scale prices to canvas height (inverted Y axis)
|
||||||
|
const highY = (maxPrice - candle.high) / priceRange * (height - 4) + 2;
|
||||||
|
const lowY = (maxPrice - candle.low) / priceRange * (height - 4) + 2;
|
||||||
|
const openY = (maxPrice - candle.open) / priceRange * (height - 4) + 2;
|
||||||
|
const closeY = (maxPrice - candle.close) / priceRange * (height - 4) + 2;
|
||||||
|
|
||||||
|
// Determine candle color
|
||||||
|
const isGreen = candle.close >= candle.open;
|
||||||
|
const color = isGreen ? '#4ecdc4' : '#ff6b6b';
|
||||||
|
|
||||||
|
// Draw high-low line
|
||||||
|
ctx.strokeStyle = color;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, highY);
|
||||||
|
ctx.lineTo(x, lowY);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Draw candle body
|
||||||
|
const bodyTop = Math.min(openY, closeY);
|
||||||
|
const bodyHeight = Math.abs(closeY - openY);
|
||||||
|
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
if (bodyHeight < 1) {
|
||||||
|
// Doji or very small body - draw as line
|
||||||
|
ctx.fillRect(x - candleWidth/2, bodyTop, candleWidth, Math.max(1, bodyHeight));
|
||||||
|
} else {
|
||||||
|
// Normal candle body
|
||||||
|
if (isGreen) {
|
||||||
|
ctx.strokeStyle = color;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.strokeRect(x - candleWidth/2, bodyTop, candleWidth, bodyHeight);
|
||||||
|
} else {
|
||||||
|
ctx.fillRect(x - candleWidth/2, bodyTop, candleWidth, bodyHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw current price line
|
||||||
|
if (ohlcvArray.length > 0) {
|
||||||
|
const lastCandle = ohlcvArray[ohlcvArray.length - 1];
|
||||||
|
const currentPriceY = (maxPrice - lastCandle.close) / priceRange * (height - 4) + 2;
|
||||||
|
|
||||||
|
ctx.strokeStyle = '#00ff88';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.setLineDash([2, 2]);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, currentPriceY);
|
||||||
|
ctx.lineTo(width, currentPriceY);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error drawing mini chart for ${prefix}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeCharts() {
|
||||||
|
// Initialize empty charts
|
||||||
|
['btc', 'eth'].forEach(prefix => {
|
||||||
|
const canvas = document.getElementById(`${prefix}-mini-chart`);
|
||||||
|
if (canvas) {
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.fillStyle = '#555';
|
||||||
|
ctx.font = '12px Courier New';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText('Waiting for data...', canvas.width / 2, canvas.height / 2);
|
||||||
|
console.log(`Initialized ${prefix} chart canvas`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize dashboard
|
// Initialize dashboard
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
updateStatus('Connecting...', false);
|
updateStatus('Connecting...', false);
|
||||||
|
|
||||||
|
// Initialize charts with placeholder
|
||||||
|
setTimeout(() => {
|
||||||
|
initializeCharts();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
// Auto-connect on load
|
// Auto-connect on load
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
@ -977,6 +1171,16 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 3000); // Check every 3 seconds
|
}, 3000); // Check every 3 seconds
|
||||||
|
|
||||||
|
// Also periodically update charts from stored data
|
||||||
|
setInterval(() => {
|
||||||
|
['BTC/USDT', 'ETH/USDT'].forEach(symbol => {
|
||||||
|
if (ohlcvData[symbol] && ohlcvData[symbol].length > 0) {
|
||||||
|
const prefix = symbol === 'BTC/USDT' ? 'btc' : 'eth';
|
||||||
|
drawMiniChart(prefix, ohlcvData[symbol]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 5000); // Update charts every 5 seconds
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
@ -62,6 +62,14 @@ class COBDashboardServer:
|
|||||||
symbol: deque(maxlen=100) for symbol in self.symbols
|
symbol: deque(maxlen=100) for symbol in self.symbols
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# OHLCV data for mini charts (5 minutes = 300 1-second candles)
|
||||||
|
self.ohlcv_data: Dict[str, deque] = {
|
||||||
|
symbol: deque(maxlen=300) for symbol in self.symbols
|
||||||
|
}
|
||||||
|
|
||||||
|
# Current candle data (building 1-second candles)
|
||||||
|
self.current_candles: Dict[str, Dict] = {}
|
||||||
|
|
||||||
# Setup routes and CORS
|
# Setup routes and CORS
|
||||||
self._setup_routes()
|
self._setup_routes()
|
||||||
self._setup_cors()
|
self._setup_cors()
|
||||||
@ -312,6 +320,9 @@ class COBDashboardServer:
|
|||||||
try:
|
try:
|
||||||
logger.debug(f"Received COB update for {symbol}")
|
logger.debug(f"Received COB update for {symbol}")
|
||||||
|
|
||||||
|
# Process OHLCV data from mid price
|
||||||
|
await self._process_ohlcv_update(symbol, data)
|
||||||
|
|
||||||
# Update cache
|
# Update cache
|
||||||
self.latest_cob_data[symbol] = data
|
self.latest_cob_data[symbol] = data
|
||||||
self.update_timestamps[symbol].append(datetime.now())
|
self.update_timestamps[symbol].append(datetime.now())
|
||||||
@ -323,17 +334,84 @@ class COBDashboardServer:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error handling COB update for {symbol}: {e}")
|
logger.error(f"Error handling COB update for {symbol}: {e}")
|
||||||
|
|
||||||
|
async def _process_ohlcv_update(self, symbol: str, data: Dict):
|
||||||
|
"""Process price updates into 1-second OHLCV candles"""
|
||||||
|
try:
|
||||||
|
stats = data.get('stats', {})
|
||||||
|
mid_price = stats.get('mid_price', 0)
|
||||||
|
|
||||||
|
if mid_price <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
current_second = now.replace(microsecond=0)
|
||||||
|
|
||||||
|
# Get or create current candle
|
||||||
|
if symbol not in self.current_candles:
|
||||||
|
self.current_candles[symbol] = {
|
||||||
|
'timestamp': current_second,
|
||||||
|
'open': mid_price,
|
||||||
|
'high': mid_price,
|
||||||
|
'low': mid_price,
|
||||||
|
'close': mid_price,
|
||||||
|
'volume': 0, # We don't have volume from order book, use tick count
|
||||||
|
'tick_count': 1
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
current_candle = self.current_candles[symbol]
|
||||||
|
|
||||||
|
# Check if we need to close current candle and start new one
|
||||||
|
if current_second > current_candle['timestamp']:
|
||||||
|
# Close previous candle
|
||||||
|
finished_candle = {
|
||||||
|
'timestamp': current_candle['timestamp'].isoformat(),
|
||||||
|
'open': current_candle['open'],
|
||||||
|
'high': current_candle['high'],
|
||||||
|
'low': current_candle['low'],
|
||||||
|
'close': current_candle['close'],
|
||||||
|
'volume': current_candle['tick_count'], # Use tick count as volume
|
||||||
|
'tick_count': current_candle['tick_count']
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add to OHLCV history
|
||||||
|
self.ohlcv_data[symbol].append(finished_candle)
|
||||||
|
|
||||||
|
# Start new candle
|
||||||
|
self.current_candles[symbol] = {
|
||||||
|
'timestamp': current_second,
|
||||||
|
'open': mid_price,
|
||||||
|
'high': mid_price,
|
||||||
|
'low': mid_price,
|
||||||
|
'close': mid_price,
|
||||||
|
'volume': 0,
|
||||||
|
'tick_count': 1
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Update current candle
|
||||||
|
current_candle['high'] = max(current_candle['high'], mid_price)
|
||||||
|
current_candle['low'] = min(current_candle['low'], mid_price)
|
||||||
|
current_candle['close'] = mid_price
|
||||||
|
current_candle['tick_count'] += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing OHLCV update for {symbol}: {e}")
|
||||||
|
|
||||||
async def _broadcast_cob_update(self, symbol: str, data: Dict):
|
async def _broadcast_cob_update(self, symbol: str, data: Dict):
|
||||||
"""Broadcast COB update to all connected WebSocket clients"""
|
"""Broadcast COB update to all connected WebSocket clients"""
|
||||||
if not self.websocket_connections:
|
if not self.websocket_connections:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Add OHLCV data to the broadcast
|
||||||
|
enhanced_data = data.copy()
|
||||||
|
if symbol in self.ohlcv_data:
|
||||||
|
enhanced_data['ohlcv'] = list(self.ohlcv_data[symbol])
|
||||||
|
|
||||||
message = {
|
message = {
|
||||||
'type': 'cob_update',
|
'type': 'cob_update',
|
||||||
'symbol': symbol,
|
'symbol': symbol,
|
||||||
'timestamp': datetime.now().isoformat(),
|
'timestamp': datetime.now().isoformat(),
|
||||||
'data': data
|
'data': enhanced_data
|
||||||
}
|
}
|
||||||
|
|
||||||
# Send to all connections
|
# Send to all connections
|
||||||
@ -382,6 +460,7 @@ class COBDashboardServer:
|
|||||||
'bids': [],
|
'bids': [],
|
||||||
'asks': [],
|
'asks': [],
|
||||||
'svp': {'data': []},
|
'svp': {'data': []},
|
||||||
|
'ohlcv': [],
|
||||||
'stats': {
|
'stats': {
|
||||||
'mid_price': 0,
|
'mid_price': 0,
|
||||||
'spread_bps': 0,
|
'spread_bps': 0,
|
||||||
|
Reference in New Issue
Block a user