COB data and dash

This commit is contained in:
Dobromir Popov
2025-06-18 16:23:47 +03:00
parent e238ce374b
commit 3cadae60f7
16 changed files with 7539 additions and 19 deletions

689
web/cob_dashboard.html Normal file
View File

@ -0,0 +1,689 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Consolidated Order Book Dashboard</title>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<style>
body {
font-family: 'Arial', sans-serif;
margin: 0;
padding: 15px;
background-color: #0a0a0a;
color: #ffffff;
overflow-x: auto;
}
.header {
text-align: center;
margin-bottom: 15px;
}
.header h1 {
color: #00ff88;
margin: 0;
font-size: 1.8rem;
}
.header .subtitle {
color: #888;
font-size: 0.9rem;
margin-top: 3px;
}
.controls {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.control-group {
display: flex;
align-items: center;
gap: 8px;
}
select, button {
background-color: #1a1a1a;
color: white;
border: 1px solid #333;
padding: 6px 10px;
border-radius: 3px;
font-size: 13px;
}
button:hover {
background-color: #2a2a2a;
cursor: pointer;
}
.status {
text-align: center;
padding: 8px;
border-radius: 4px;
margin-bottom: 15px;
font-weight: bold;
font-size: 0.9rem;
}
.status.connected {
background-color: #0a2a0a;
color: #00ff88;
border: 1px solid #00ff88;
}
.status.disconnected {
background-color: #2a0a0a;
color: #ff4444;
border: 1px solid #ff4444;
}
.dashboard-container {
display: grid;
grid-template-columns: 1fr 400px;
grid-template-rows: 1fr auto;
gap: 15px;
height: calc(100vh - 150px);
}
.chart-section {
display: flex;
flex-direction: column;
gap: 15px;
}
.price-chart-container {
background-color: #1a1a1a;
border-radius: 6px;
padding: 10px;
border: 1px solid #333;
flex: 1;
}
.svp-chart-container {
background-color: #1a1a1a;
border-radius: 6px;
padding: 10px;
border: 1px solid #333;
height: 200px;
}
.orderbook-section {
background-color: #1a1a1a;
border-radius: 6px;
padding: 10px;
border: 1px solid #333;
display: flex;
flex-direction: column;
}
.chart-title {
color: #00ff88;
font-size: 1rem;
font-weight: bold;
margin-bottom: 8px;
text-align: center;
}
.orderbook-header {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
padding: 8px 0;
border-bottom: 1px solid #333;
margin-bottom: 10px;
font-size: 0.85rem;
font-weight: bold;
color: #888;
}
.orderbook-content {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.asks-section, .bids-section {
flex: 1;
overflow-y: auto;
}
.mid-price-section {
padding: 10px 0;
text-align: center;
border-top: 1px solid #333;
border-bottom: 1px solid #333;
margin: 5px 0;
}
.mid-price {
font-size: 1.2rem;
font-weight: bold;
color: #00ff88;
}
.spread {
font-size: 0.8rem;
color: #888;
margin-top: 2px;
}
.orderbook-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 5px;
padding: 2px 0;
font-size: 0.8rem;
cursor: pointer;
}
.orderbook-row:hover {
background-color: #2a2a2a;
}
.ask-row {
color: #ff6b6b;
}
.bid-row {
color: #4ecdc4;
}
.price-cell {
text-align: right;
font-weight: bold;
}
.size-cell {
text-align: right;
}
.total-cell {
text-align: right;
color: #888;
}
.volume-bar {
position: absolute;
top: 0;
right: 0;
height: 100%;
opacity: 0.1;
z-index: -1;
}
.ask-bar {
background-color: #ff6b6b;
}
.bid-bar {
background-color: #4ecdc4;
}
.stats-container {
grid-column: span 2;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
}
.stat-card {
background-color: #1a1a1a;
border-radius: 6px;
padding: 12px;
border: 1px solid #333;
text-align: center;
}
.stat-label {
color: #888;
font-size: 0.8rem;
margin-bottom: 4px;
}
.stat-value {
color: #00ff88;
font-size: 1.2rem;
font-weight: bold;
}
.stat-sub {
color: #ccc;
font-size: 0.7rem;
margin-top: 3px;
}
#price-chart, #svp-chart {
height: 100%;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: #888;
font-size: 1rem;
}
</style>
</head>
<body>
<div class="header">
<h1>Consolidated Order Book Dashboard</h1>
<div class="subtitle">Hybrid WebSocket + REST API | Real-time + Deep Market Data</div>
</div>
<div class="controls">
<div class="control-group">
<label for="symbolSelect">Symbol:</label>
<select id="symbolSelect">
<option value="BTC/USDT">BTC/USDT</option>
<option value="ETH/USDT">ETH/USDT</option>
</select>
</div>
<div class="control-group">
<button onclick="toggleConnection()">Toggle Connection</button>
<button onclick="refreshData()">Refresh Data</button>
</div>
</div>
<div id="status" class="status disconnected">
Disconnected - Click Toggle Connection to start
</div>
<div class="dashboard-container">
<!-- Charts Section -->
<div class="chart-section">
<!-- Price Chart -->
<div class="price-chart-container">
<div class="chart-title">Price & Volume Analysis</div>
<div id="price-chart"></div>
</div>
<!-- Session Volume Profile -->
<div class="svp-chart-container">
<div class="chart-title">Session Volume Profile (SVP)</div>
<div id="svp-chart"></div>
</div>
</div>
<!-- Order Book Ladder -->
<div class="orderbook-section">
<div class="chart-title">Order Book Ladder</div>
<div class="orderbook-header">
<div>Price</div>
<div>Size</div>
<div>Total</div>
</div>
<div class="orderbook-content">
<!-- Asks (Sells) - Top half -->
<div class="asks-section" id="asks-section">
<div class="loading">Loading asks...</div>
</div>
<!-- Mid Price -->
<div class="mid-price-section">
<div class="mid-price" id="mid-price-display">$--</div>
<div class="spread" id="spread-display">Spread: -- bps</div>
</div>
<!-- Bids (Buys) - Bottom half -->
<div class="bids-section" id="bids-section">
<div class="loading">Loading bids...</div>
</div>
</div>
</div>
<!-- Statistics -->
<div class="stats-container">
<div class="stat-card">
<div class="stat-label">Total Liquidity</div>
<div class="stat-value" id="total-liquidity">--</div>
<div class="stat-sub" id="liquidity-breakdown">--</div>
</div>
<div class="stat-card">
<div class="stat-label">Book Depth</div>
<div class="stat-value" id="book-depth">--</div>
<div class="stat-sub" id="depth-breakdown">--</div>
</div>
<div class="stat-card">
<div class="stat-label">Imbalance</div>
<div class="stat-value" id="imbalance">--</div>
<div class="stat-sub">bid/ask ratio</div>
</div>
<div class="stat-card">
<div class="stat-label">Update Rate</div>
<div class="stat-value" id="update-rate">--</div>
<div class="stat-sub">updates/sec</div>
</div>
<div class="stat-card">
<div class="stat-label">Best Bid</div>
<div class="stat-value" id="best-bid">--</div>
<div class="stat-sub" id="bid-size">--</div>
</div>
<div class="stat-card">
<div class="stat-label">Best Ask</div>
<div class="stat-value" id="best-ask">--</div>
<div class="stat-sub" id="ask-size">--</div>
</div>
</div>
</div>
<script>
let ws = null;
let currentSymbol = 'BTC/USDT';
let isConnected = false;
let lastUpdateTime = 0;
let updateCount = 0;
let currentData = null;
// Initialize charts
function initializeCharts() {
// Price Chart Layout
const priceLayout = {
title: '',
xaxis: {
title: 'Time',
color: '#ffffff',
gridcolor: '#333',
showgrid: false
},
yaxis: {
title: 'Price',
color: '#ffffff',
gridcolor: '#333'
},
plot_bgcolor: '#0a0a0a',
paper_bgcolor: '#1a1a1a',
font: { color: '#ffffff' },
margin: { l: 60, r: 20, t: 20, b: 40 },
showlegend: false
};
// SVP Chart Layout
const svpLayout = {
title: '',
xaxis: {
title: 'Volume',
color: '#ffffff',
gridcolor: '#333'
},
yaxis: {
title: 'Price',
color: '#ffffff',
gridcolor: '#333'
},
plot_bgcolor: '#0a0a0a',
paper_bgcolor: '#1a1a1a',
font: { color: '#ffffff' },
margin: { l: 60, r: 20, t: 20, b: 40 }
};
// Initialize empty charts
Plotly.newPlot('price-chart', [], priceLayout, {responsive: true});
Plotly.newPlot('svp-chart', [], svpLayout, {responsive: true});
}
function connectWebSocket() {
if (ws) {
ws.close();
}
ws = new WebSocket(`ws://${window.location.host}/ws`);
ws.onopen = function() {
console.log('WebSocket connected');
isConnected = true;
updateStatus('Connected - Receiving real-time data', true);
};
ws.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
console.log('Received data:', data);
if (data.type === 'cob_update') {
handleCOBUpdate(data);
}
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
};
ws.onclose = function() {
console.log('WebSocket disconnected');
isConnected = false;
updateStatus('Disconnected - Click Toggle Connection to reconnect', false);
};
ws.onerror = function(error) {
console.error('WebSocket error:', error);
updateStatus('Connection Error - Check server status', false);
};
}
function handleCOBUpdate(data) {
// Handle nested data structure from API
if (data.data && data.data.data) {
currentData = data.data.data;
} else if (data.data) {
currentData = data.data;
} else {
currentData = data;
}
console.log('Processing COB data:', currentData);
const stats = currentData.stats || {};
// Update statistics
updateStatistics(stats);
// Update order book ladder
updateOrderBookLadder(currentData);
// Update SVP chart
updateSVPChart(currentData);
// Track update rate
updateCount++;
const now = Date.now();
if (now - lastUpdateTime >= 1000) {
document.getElementById('update-rate').textContent = updateCount;
updateCount = 0;
lastUpdateTime = now;
}
}
function updateOrderBookLadder(cobData) {
const bids = cobData.bids || [];
const asks = cobData.asks || [];
// Sort asks (lowest price first, closest to mid)
const sortedAsks = [...asks].sort((a, b) => a.price - b.price);
// Sort bids (highest price first, closest to mid)
const sortedBids = [...bids].sort((a, b) => b.price - a.price);
// Update asks section (top half)
const asksSection = document.getElementById('asks-section');
asksSection.innerHTML = '';
// Show asks in reverse order (highest first, then down to mid)
sortedAsks.reverse().forEach((ask, index) => {
const row = createOrderBookRow(ask, 'ask');
asksSection.appendChild(row);
});
// Update bids section (bottom half)
const bidsSection = document.getElementById('bids-section');
bidsSection.innerHTML = '';
// Show bids in normal order (highest first, then down)
sortedBids.forEach((bid, index) => {
const row = createOrderBookRow(bid, 'bid');
bidsSection.appendChild(row);
});
// Update mid price
const stats = cobData.stats || {};
document.getElementById('mid-price-display').textContent = `$${(stats.mid_price || 0).toFixed(2)}`;
document.getElementById('spread-display').textContent = `Spread: ${(stats.spread_bps || 0).toFixed(2)} bps`;
}
function createOrderBookRow(data, type) {
const row = document.createElement('div');
row.className = `orderbook-row ${type}-row`;
row.style.position = 'relative';
// Calculate total volume for bar width
const maxVolume = currentData ? Math.max(
...currentData.bids.map(b => b.volume),
...currentData.asks.map(a => a.volume)
) : 1;
const barWidth = (data.volume / maxVolume) * 100;
row.innerHTML = `
<div class="volume-bar ${type}-bar" style="width: ${barWidth}%"></div>
<div class="price-cell">$${data.price.toFixed(2)}</div>
<div class="size-cell">${(data.volume || 0).toFixed(4)}</div>
<div class="total-cell">$${((data.volume || 0) / 1000).toFixed(0)}K</div>
`;
return row;
}
function updateStatistics(stats) {
// Total Liquidity
const totalLiq = (stats.bid_liquidity + stats.ask_liquidity) || 0;
document.getElementById('total-liquidity').textContent = `$${(totalLiq / 1000).toFixed(0)}K`;
document.getElementById('liquidity-breakdown').textContent =
`Bid: $${(stats.bid_liquidity / 1000).toFixed(0)}K | Ask: $${(stats.ask_liquidity / 1000).toFixed(0)}K`;
// Order Book Depth
const bidCount = stats.bid_levels || 0;
const askCount = stats.ask_levels || 0;
document.getElementById('book-depth').textContent = `${bidCount + askCount}`;
document.getElementById('depth-breakdown').textContent = `${bidCount} bids | ${askCount} asks`;
// Imbalance
const imbalance = stats.imbalance || 0;
document.getElementById('imbalance').textContent = `${(imbalance * 100).toFixed(1)}%`;
// Best Bid/Ask
if (currentData && currentData.bids && currentData.bids.length > 0) {
const bestBid = Math.max(...currentData.bids.map(b => b.price));
const bestBidData = currentData.bids.find(b => b.price === bestBid);
document.getElementById('best-bid').textContent = `$${bestBid.toFixed(2)}`;
document.getElementById('bid-size').textContent = `${(bestBidData.volume || 0).toFixed(4)}`;
}
if (currentData && currentData.asks && currentData.asks.length > 0) {
const bestAsk = Math.min(...currentData.asks.map(a => a.price));
const bestAskData = currentData.asks.find(a => a.price === bestAsk);
document.getElementById('best-ask').textContent = `$${bestAsk.toFixed(2)}`;
document.getElementById('ask-size').textContent = `${(bestAskData.volume || 0).toFixed(4)}`;
}
}
function updateSVPChart(cobData) {
const svp = cobData.svp || [];
// Handle both array format and object format
let svpData = [];
if (Array.isArray(svp)) {
svpData = svp;
} else if (svp.data && Array.isArray(svp.data)) {
svpData = svp.data;
}
if (svpData.length === 0) {
console.log('No SVP data available');
return;
}
// Prepare SVP data
const buyTrace = {
x: svpData.map(d => -d.buy_volume),
y: svpData.map(d => d.price),
type: 'bar',
orientation: 'h',
name: 'Buy Volume',
marker: { color: 'rgba(78, 205, 196, 0.7)' },
hovertemplate: 'Price: $%{y}<br>Buy Volume: $%{customdata:,.0f}<extra></extra>',
customdata: svpData.map(d => d.buy_volume)
};
const sellTrace = {
x: svpData.map(d => d.sell_volume),
y: svpData.map(d => d.price),
type: 'bar',
orientation: 'h',
name: 'Sell Volume',
marker: { color: 'rgba(255, 107, 107, 0.7)' },
hovertemplate: 'Price: $%{y}<br>Sell Volume: $%{x:,.0f}<extra></extra>'
};
Plotly.redraw('svp-chart', [buyTrace, sellTrace]);
}
function updateStatus(message, connected) {
const statusEl = document.getElementById('status');
statusEl.textContent = message;
statusEl.className = `status ${connected ? 'connected' : 'disconnected'}`;
}
function toggleConnection() {
if (isConnected) {
if (ws) ws.close();
} else {
connectWebSocket();
}
}
function refreshData() {
if (isConnected) {
// Request fresh data from API
fetch(`/api/cob/${encodeURIComponent(currentSymbol)}`)
.then(response => response.json())
.then(data => {
console.log('Refreshed data:', data);
if (data.data) {
handleCOBUpdate({type: 'cob_update', data: data.data});
}
})
.catch(error => console.error('Error refreshing data:', error));
}
}
// Symbol change handler
document.getElementById('symbolSelect').addEventListener('change', function() {
currentSymbol = this.value;
console.log('Symbol changed to:', currentSymbol);
refreshData();
});
// Initialize dashboard
document.addEventListener('DOMContentLoaded', function() {
initializeCharts();
updateStatus('Connecting...', false);
// Auto-connect on load
setTimeout(() => {
connectWebSocket();
}, 500);
});
</script>
</body>
</html>

View File

@ -0,0 +1,479 @@
#!/usr/bin/env python3
"""
Consolidated Order Book (COB) Real-time Dashboard Server
Provides a web interface for visualizing:
- Consolidated order book across multiple exchanges
- Session Volume Profile (SVP) from actual trades
- Real-time statistics for neural network models
- Hybrid WebSocket + REST API order book data
Windows-compatible implementation with proper error handling.
"""
import asyncio
import json
import logging
import weakref
from datetime import datetime, timedelta
from collections import deque
from typing import Dict, List, Optional, Any
import traceback
# Windows-compatible imports
try:
from aiohttp import web, WSMsgType
import aiohttp_cors
except ImportError as e:
logging.error(f"Required dependencies missing: {e}")
raise
from core.cob_integration import COBIntegration
logger = logging.getLogger(__name__)
class COBDashboardServer:
"""
Real-time COB Dashboard Server with Windows compatibility
"""
def __init__(self, host: str = 'localhost', port: int = 8053):
self.host = host
self.port = port
self.app = web.Application()
self.symbols = ['BTC/USDT', 'ETH/USDT']
# COB components
self.cob_integration: Optional[COBIntegration] = None
# Web server components
self.runner = None
self.site = None
# WebSocket connections for real-time updates
self.websocket_connections = weakref.WeakSet()
# Latest data cache for quick serving
self.latest_cob_data: Dict[str, Dict] = {}
self.latest_stats: Dict = {}
# Update timestamps for monitoring
self.update_timestamps: Dict[str, deque] = {
symbol: deque(maxlen=100) for symbol in self.symbols
}
# Setup routes and CORS
self._setup_routes()
self._setup_cors()
logger.info(f"COB Dashboard Server initialized for {self.symbols}")
def _setup_routes(self):
"""Setup HTTP routes"""
# Static files
self.app.router.add_get('/', self.serve_dashboard)
# API endpoints
self.app.router.add_get('/api/symbols', self.get_symbols)
self.app.router.add_get('/api/cob/{symbol}', self.get_cob_data)
self.app.router.add_get('/api/realtime/{symbol}', self.get_realtime_stats)
self.app.router.add_get('/api/status', self.get_status)
# WebSocket endpoint
self.app.router.add_get('/ws', self.websocket_handler)
def _setup_cors(self):
"""Setup CORS for cross-origin requests"""
cors = aiohttp_cors.setup(self.app, defaults={
"*": aiohttp_cors.ResourceOptions(
allow_credentials=True,
expose_headers="*",
allow_headers="*",
allow_methods="*"
)
})
# Add CORS to all routes
for route in list(self.app.router.routes()):
cors.add(route)
async def start(self):
"""Start the dashboard server"""
try:
logger.info(f"Starting COB Dashboard Server on {self.host}:{self.port}")
# Start web server first
self.runner = web.AppRunner(self.app)
await self.runner.setup()
self.site = web.TCPSite(self.runner, self.host, self.port)
await self.site.start()
logger.info(f"COB Dashboard Server running at http://{self.host}:{self.port}")
# Initialize COB integration
self.cob_integration = COBIntegration(symbols=self.symbols)
self.cob_integration.add_dashboard_callback(self._on_cob_update)
# Start COB data streaming as background task
asyncio.create_task(self.cob_integration.start())
# Start periodic tasks as background tasks
asyncio.create_task(self._periodic_stats_update())
asyncio.create_task(self._cleanup_old_data())
# Keep the server running
while True:
await asyncio.sleep(1)
except Exception as e:
logger.error(f"Error starting COB Dashboard Server: {e}")
logger.error(traceback.format_exc())
raise
async def stop(self):
"""Stop the dashboard server"""
logger.info("Stopping COB Dashboard Server")
# Close all WebSocket connections
for ws in list(self.websocket_connections):
try:
await ws.close()
except Exception as e:
logger.warning(f"Error closing WebSocket: {e}")
# Stop web server
if self.site:
await self.site.stop()
if self.runner:
await self.runner.cleanup()
# Stop COB integration
if self.cob_integration:
await self.cob_integration.stop()
logger.info("COB Dashboard Server stopped")
async def serve_dashboard(self, request):
"""Serve the main dashboard HTML page"""
try:
return web.FileResponse('web/cob_dashboard.html')
except FileNotFoundError:
return web.Response(
text="Dashboard HTML file not found",
status=404,
content_type='text/plain'
)
async def get_symbols(self, request):
"""Get available symbols"""
return web.json_response({
'symbols': self.symbols,
'default': self.symbols[0] if self.symbols else None
})
async def get_cob_data(self, request):
"""Get consolidated order book data for a symbol"""
try:
symbol = request.match_info['symbol']
symbol = symbol.replace('%2F', '/') # URL decode
if symbol not in self.symbols:
return web.json_response({
'error': f'Symbol {symbol} not supported',
'available_symbols': self.symbols
}, status=400)
# Get latest data from cache or COB integration
if symbol in self.latest_cob_data:
data = self.latest_cob_data[symbol]
elif self.cob_integration:
data = await self._generate_dashboard_data(symbol)
else:
data = self._get_empty_data(symbol)
return web.json_response({
'symbol': symbol,
'timestamp': datetime.now().isoformat(),
'data': data
})
except Exception as e:
logger.error(f"Error getting COB data: {e}")
return web.json_response({
'error': str(e)
}, status=500)
async def get_realtime_stats(self, request):
"""Get real-time statistics for neural network models"""
try:
symbol = request.match_info['symbol']
symbol = symbol.replace('%2F', '/')
if symbol not in self.symbols:
return web.json_response({
'error': f'Symbol {symbol} not supported'
}, status=400)
stats = {}
if self.cob_integration:
stats = self.cob_integration.get_realtime_stats_for_nn(symbol)
return web.json_response({
'symbol': symbol,
'timestamp': datetime.now().isoformat(),
'stats': stats
})
except Exception as e:
logger.error(f"Error getting realtime stats: {e}")
return web.json_response({
'error': str(e)
}, status=500)
async def get_status(self, request):
"""Get server status"""
status = {
'server': 'running',
'symbols': self.symbols,
'websocket_connections': len(self.websocket_connections),
'cob_integration': 'active' if self.cob_integration else 'inactive',
'last_updates': {}
}
# Add last update times
for symbol in self.symbols:
if symbol in self.update_timestamps and self.update_timestamps[symbol]:
status['last_updates'][symbol] = self.update_timestamps[symbol][-1].isoformat()
return web.json_response(status)
async def websocket_handler(self, request):
"""Handle WebSocket connections"""
ws = web.WebSocketResponse()
await ws.prepare(request)
# Add to connections
self.websocket_connections.add(ws)
logger.info(f"WebSocket connected. Total connections: {len(self.websocket_connections)}")
try:
# Send initial data
for symbol in self.symbols:
if symbol in self.latest_cob_data:
await self._send_websocket_data(ws, 'cob_update', symbol, self.latest_cob_data[symbol])
# Handle incoming messages
async for msg in ws:
if msg.type == WSMsgType.TEXT:
try:
data = json.loads(msg.data)
await self._handle_websocket_message(ws, data)
except json.JSONDecodeError:
await ws.send_str(json.dumps({
'type': 'error',
'message': 'Invalid JSON'
}))
elif msg.type == WSMsgType.ERROR:
logger.error(f'WebSocket error: {ws.exception()}')
break
except Exception as e:
logger.error(f"WebSocket error: {e}")
finally:
# Remove from connections
self.websocket_connections.discard(ws)
logger.info(f"WebSocket disconnected. Remaining connections: {len(self.websocket_connections)}")
return ws
async def _handle_websocket_message(self, ws, data):
"""Handle incoming WebSocket messages"""
try:
message_type = data.get('type')
if message_type == 'subscribe':
symbol = data.get('symbol')
if symbol in self.symbols and symbol in self.latest_cob_data:
await self._send_websocket_data(ws, 'cob_update', symbol, self.latest_cob_data[symbol])
elif message_type == 'ping':
await ws.send_str(json.dumps({
'type': 'pong',
'timestamp': datetime.now().isoformat()
}))
except Exception as e:
logger.error(f"Error handling WebSocket message: {e}")
async def _on_cob_update(self, symbol: str, data: Dict):
"""Handle COB updates from integration"""
try:
logger.debug(f"Received COB update for {symbol}")
# Update cache
self.latest_cob_data[symbol] = data
self.update_timestamps[symbol].append(datetime.now())
# Broadcast to WebSocket clients
await self._broadcast_cob_update(symbol, data)
logger.debug(f"Broadcasted COB update for {symbol} to {len(self.websocket_connections)} connections")
except Exception as e:
logger.error(f"Error handling COB 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
message = {
'type': 'cob_update',
'symbol': symbol,
'timestamp': datetime.now().isoformat(),
'data': data
}
# Send to all connections
dead_connections = []
for ws in self.websocket_connections:
try:
await ws.send_str(json.dumps(message))
except Exception as e:
logger.warning(f"Failed to send to WebSocket: {e}")
dead_connections.append(ws)
# Clean up dead connections
for ws in dead_connections:
self.websocket_connections.discard(ws)
async def _send_websocket_data(self, ws, msg_type: str, symbol: str, data: Dict):
"""Send data to a specific WebSocket connection"""
try:
message = {
'type': msg_type,
'symbol': symbol,
'timestamp': datetime.now().isoformat(),
'data': data
}
await ws.send_str(json.dumps(message))
except Exception as e:
logger.error(f"Error sending WebSocket data: {e}")
async def _generate_dashboard_data(self, symbol: str) -> Dict:
"""Generate dashboard data for a symbol"""
try:
# Return cached data from COB integration callbacks
if symbol in self.latest_cob_data:
return self.latest_cob_data[symbol]
else:
return self._get_empty_data(symbol)
except Exception as e:
logger.error(f"Error generating dashboard data for {symbol}: {e}")
return self._get_empty_data(symbol)
def _get_empty_data(self, symbol: str) -> Dict:
"""Get empty data structure"""
return {
'symbol': symbol,
'bids': [],
'asks': [],
'svp': {'data': []},
'stats': {
'mid_price': 0,
'spread_bps': 0,
'bid_liquidity': 0,
'ask_liquidity': 0,
'bid_levels': 0,
'ask_levels': 0,
'imbalance': 0
}
}
async def _periodic_stats_update(self):
"""Periodically update and broadcast statistics"""
while True:
try:
# Calculate update frequencies
update_frequencies = {}
for symbol in self.symbols:
if symbol in self.update_timestamps and len(self.update_timestamps[symbol]) > 1:
timestamps = list(self.update_timestamps[symbol])
if len(timestamps) >= 2:
time_diff = (timestamps[-1] - timestamps[-2]).total_seconds()
if time_diff > 0:
update_frequencies[symbol] = 1.0 / time_diff
# Broadcast stats if needed
if update_frequencies:
stats_message = {
'type': 'stats_update',
'timestamp': datetime.now().isoformat(),
'update_frequencies': update_frequencies
}
for ws in list(self.websocket_connections):
try:
await ws.send_str(json.dumps(stats_message))
except Exception:
self.websocket_connections.discard(ws)
await asyncio.sleep(5) # Update every 5 seconds
except Exception as e:
logger.error(f"Error in periodic stats update: {e}")
await asyncio.sleep(5)
async def _cleanup_old_data(self):
"""Clean up old data to prevent memory leaks"""
while True:
try:
cutoff_time = datetime.now() - timedelta(hours=1)
# Clean up old timestamps
for symbol in self.symbols:
if symbol in self.update_timestamps:
timestamps = self.update_timestamps[symbol]
while timestamps and timestamps[0] < cutoff_time:
timestamps.popleft()
await asyncio.sleep(300) # Clean up every 5 minutes
except Exception as e:
logger.error(f"Error in cleanup: {e}")
await asyncio.sleep(300)
async def main():
"""Main entry point"""
# Set up logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger.info("Starting COB Dashboard Server")
try:
# Windows event loop policy fix
if hasattr(asyncio, 'WindowsProactorEventLoopPolicy'):
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
server = COBDashboardServer()
await server.start()
except KeyboardInterrupt:
logger.info("COB Dashboard Server interrupted by user")
except Exception as e:
logger.error(f"COB Dashboard Server failed: {e}")
logger.error(traceback.format_exc())
finally:
if 'server' in locals():
await server.stop()
if __name__ == "__main__":
asyncio.run(main())