small ui fixes

This commit is contained in:
Dobromir Popov
2025-06-18 20:17:11 +03:00
parent f1ef2702d7
commit 72b010631a
2 changed files with 298 additions and 15 deletions

View File

@ -154,6 +154,35 @@
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 {
display: grid;
grid-template-columns: 60px 100px 1fr 80px;
@ -386,8 +415,8 @@
<div class="orderbooks-container">
<!-- BTC Order Book -->
<div class="orderbook-panel">
<div class="orderbook-title">BTC/USDT</div>
<div class="price-resolution">Resolution: $10 buckets</div>
<div class="orderbook-title" id="btc-title">BTC/USDT - $--</div>
<div class="price-resolution" id="btc-resolution">Resolution: $10 buckets</div>
<div class="orderbook-header">
<div>Side</div>
@ -426,8 +455,8 @@
<!-- ETH Order Book -->
<div class="orderbook-panel">
<div class="orderbook-title">ETH/USDT</div>
<div class="price-resolution">Resolution: $1 buckets</div>
<div class="orderbook-title" id="eth-title">ETH/USDT - $--</div>
<div class="price-resolution" id="eth-resolution">Resolution: $1 buckets</div>
<div class="orderbook-header">
<div>Side</div>
@ -494,6 +523,12 @@
}
};
// OHLCV data storage for mini charts
let ohlcvData = {
'BTC/USDT': [],
'ETH/USDT': []
};
function connectWebSocket() {
if (ws) {
ws.close();
@ -550,7 +585,10 @@
asksCount: (cobData.asks || []).length,
sampleBid: (cobData.bids || [])[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
@ -565,6 +603,20 @@
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
const stats = cobData.stats || {};
if (stats.imbalance !== undefined) {
@ -645,13 +697,21 @@
return Math.round(price / bucketSize) * bucketSize;
}
function updateOrderBook(prefix, cobData, resolutionFunc) {
const bids = cobData.bids || [];
const asks = cobData.asks || [];
const stats = cobData.stats || {};
const midPrice = stats.mid_price || 0;
function updateOrderBook(prefix, cobData, resolutionFunc) {
const bids = cobData.bids || [];
const asks = cobData.asks || [];
const stats = cobData.stats || {};
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
const baseRange = 0.02; // 2% base range
@ -856,9 +916,13 @@
maximumFractionDigits: 2
});
const spreadText = data.spread ? `${data.spread.toFixed(1)} bps` : '--';
row.innerHTML = `
<div class="mid-price">$${priceFormatted}</div>
<div class="spread">Spread: ${data.spread.toFixed(2)} bps</div>
<div class="chart-title">1s OHLCV (5min)</div>
<div class="mini-chart">
<canvas id="${prefix}-mini-chart" width="200" height="60"></canvas>
</div>
`;
return row;
@ -945,6 +1009,10 @@
document.querySelector('.subtitle').textContent =
`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
if (currentData['BTC/USDT']) {
updateOrderBook('btc', currentData['BTC/USDT'], getBTCResolution);
@ -956,10 +1024,136 @@
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
document.addEventListener('DOMContentLoaded', function() {
updateStatus('Connecting...', false);
// Initialize charts with placeholder
setTimeout(() => {
initializeCharts();
}, 100);
// Auto-connect on load
setTimeout(() => {
connectWebSocket();
@ -977,6 +1171,16 @@
});
}
}, 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>
</body>

View File

@ -62,6 +62,14 @@ class COBDashboardServer:
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
self._setup_routes()
self._setup_cors()
@ -312,6 +320,9 @@ class COBDashboardServer:
try:
logger.debug(f"Received COB update for {symbol}")
# Process OHLCV data from mid price
await self._process_ohlcv_update(symbol, data)
# Update cache
self.latest_cob_data[symbol] = data
self.update_timestamps[symbol].append(datetime.now())
@ -324,16 +335,83 @@ class COBDashboardServer:
except Exception as 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):
"""Broadcast COB update to all connected WebSocket clients"""
if not self.websocket_connections:
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 = {
'type': 'cob_update',
'symbol': symbol,
'timestamp': datetime.now().isoformat(),
'data': data
'data': enhanced_data
}
# Send to all connections
@ -382,6 +460,7 @@ class COBDashboardServer:
'bids': [],
'asks': [],
'svp': {'data': []},
'ohlcv': [],
'stats': {
'mid_price': 0,
'spread_bps': 0,