small ui fixes
This commit is contained in:
@ -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) {
|
||||
@ -653,6 +705,14 @@
|
||||
|
||||
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
|
||||
const expandedRange = baseRange * Math.max(1, resolutionMultiplier * 2.5); // Very aggressive expansion
|
||||
@ -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>
|
||||
|
@ -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,
|
||||
|
Reference in New Issue
Block a user