COB data and dash
This commit is contained in:
401
simple_cob_dashboard.py
Normal file
401
simple_cob_dashboard.py
Normal file
@ -0,0 +1,401 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple Windows-compatible COB Dashboard
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||
from socketserver import ThreadingMixIn
|
||||
import threading
|
||||
import webbrowser
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
from core.multi_exchange_cob_provider import MultiExchangeCOBProvider
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class COBHandler(SimpleHTTPRequestHandler):
|
||||
"""HTTP handler for COB dashboard"""
|
||||
|
||||
def __init__(self, *args, cob_provider=None, **kwargs):
|
||||
self.cob_provider = cob_provider
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def do_GET(self):
|
||||
"""Handle GET requests"""
|
||||
path = urlparse(self.path).path
|
||||
|
||||
if path == '/':
|
||||
self.serve_dashboard()
|
||||
elif path.startswith('/api/cob/'):
|
||||
self.serve_cob_data()
|
||||
elif path == '/api/status':
|
||||
self.serve_status()
|
||||
else:
|
||||
super().do_GET()
|
||||
|
||||
def serve_dashboard(self):
|
||||
"""Serve the dashboard HTML"""
|
||||
html_content = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>COB Dashboard</title>
|
||||
<style>
|
||||
body { font-family: Arial; background: #1a1a1a; color: white; margin: 20px; }
|
||||
.header { text-align: center; margin-bottom: 20px; }
|
||||
.header h1 { color: #00ff88; }
|
||||
.container { display: grid; grid-template-columns: 1fr 400px; gap: 20px; }
|
||||
.chart-section { background: #2a2a2a; padding: 15px; border-radius: 8px; }
|
||||
.orderbook-section { background: #2a2a2a; padding: 15px; border-radius: 8px; }
|
||||
.orderbook-header { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px;
|
||||
padding: 10px 0; border-bottom: 1px solid #444; font-weight: bold; }
|
||||
.orderbook-row { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px;
|
||||
padding: 3px 0; font-size: 0.9rem; }
|
||||
.ask-row { color: #ff6b6b; }
|
||||
.bid-row { color: #4ecdc4; }
|
||||
.mid-price { text-align: center; padding: 15px; border: 1px solid #444;
|
||||
margin: 10px 0; font-size: 1.2rem; font-weight: bold; color: #00ff88; }
|
||||
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 20px; }
|
||||
.stat-card { background: #2a2a2a; padding: 15px; border-radius: 8px; text-align: center; }
|
||||
.stat-label { color: #888; font-size: 0.9rem; }
|
||||
.stat-value { color: #00ff88; font-size: 1.3rem; font-weight: bold; }
|
||||
.controls { text-align: center; margin-bottom: 20px; }
|
||||
button { background: #333; color: white; border: 1px solid #555; padding: 8px 15px;
|
||||
border-radius: 4px; margin: 0 5px; cursor: pointer; }
|
||||
button:hover { background: #444; }
|
||||
.status { padding: 10px; text-align: center; border-radius: 4px; margin-bottom: 20px; }
|
||||
.connected { background: #1a4a1a; color: #00ff88; border: 1px solid #00ff88; }
|
||||
.disconnected { background: #4a1a1a; color: #ff4444; border: 1px solid #ff4444; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>Consolidated Order Book Dashboard</h1>
|
||||
<div>Hybrid WebSocket + REST API | Real-time + Deep Market Data</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="refreshData()">Refresh Data</button>
|
||||
<button onclick="toggleSymbol()">Switch Symbol</button>
|
||||
</div>
|
||||
|
||||
<div id="status" class="status disconnected">Loading...</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="chart-section">
|
||||
<h3>Market Analysis</h3>
|
||||
<div id="chart-placeholder">
|
||||
<p>Chart data will be displayed here</p>
|
||||
<div>Current implementation shows:</div>
|
||||
<ul>
|
||||
<li>✓ Real-time order book data (WebSocket)</li>
|
||||
<li>✓ Deep market data (REST API)</li>
|
||||
<li>✓ Session Volume Profile</li>
|
||||
<li>✓ Hybrid data merging</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="orderbook-section">
|
||||
<h3>Order Book Ladder</h3>
|
||||
|
||||
<div class="orderbook-header">
|
||||
<div>Price</div>
|
||||
<div>Size</div>
|
||||
<div>Total</div>
|
||||
</div>
|
||||
|
||||
<div id="asks-section"></div>
|
||||
|
||||
<div class="mid-price" id="mid-price">$--</div>
|
||||
|
||||
<div id="bids-section"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Total Liquidity</div>
|
||||
<div class="stat-value" id="total-liquidity">--</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Book Depth</div>
|
||||
<div class="stat-value" id="book-depth">--</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Spread</div>
|
||||
<div class="stat-value" id="spread">-- bps</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentSymbol = 'BTC/USDT';
|
||||
|
||||
function refreshData() {
|
||||
document.getElementById('status').textContent = 'Refreshing...';
|
||||
|
||||
fetch(`/api/cob/${encodeURIComponent(currentSymbol)}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
updateOrderBook(data);
|
||||
updateStatus('Connected - Data updated', true);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
updateStatus('Error loading data', false);
|
||||
});
|
||||
}
|
||||
|
||||
function updateOrderBook(data) {
|
||||
const bids = data.bids || [];
|
||||
const asks = data.asks || [];
|
||||
const stats = data.stats || {};
|
||||
|
||||
// Update asks section
|
||||
const asksSection = document.getElementById('asks-section');
|
||||
asksSection.innerHTML = '';
|
||||
asks.sort((a, b) => a.price - b.price).reverse().forEach(ask => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'orderbook-row ask-row';
|
||||
row.innerHTML = `
|
||||
<div>$${ask.price.toFixed(2)}</div>
|
||||
<div>${ask.size.toFixed(4)}</div>
|
||||
<div>$${(ask.volume/1000).toFixed(0)}K</div>
|
||||
`;
|
||||
asksSection.appendChild(row);
|
||||
});
|
||||
|
||||
// Update bids section
|
||||
const bidsSection = document.getElementById('bids-section');
|
||||
bidsSection.innerHTML = '';
|
||||
bids.sort((a, b) => b.price - a.price).forEach(bid => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'orderbook-row bid-row';
|
||||
row.innerHTML = `
|
||||
<div>$${bid.price.toFixed(2)}</div>
|
||||
<div>${bid.size.toFixed(4)}</div>
|
||||
<div>$${(bid.volume/1000).toFixed(0)}K</div>
|
||||
`;
|
||||
bidsSection.appendChild(row);
|
||||
});
|
||||
|
||||
// Update mid price
|
||||
document.getElementById('mid-price').textContent = `$${(stats.mid_price || 0).toFixed(2)}`;
|
||||
|
||||
// Update stats
|
||||
const totalLiq = (stats.bid_liquidity + stats.ask_liquidity) || 0;
|
||||
document.getElementById('total-liquidity').textContent = `$${(totalLiq/1000).toFixed(0)}K`;
|
||||
document.getElementById('book-depth').textContent = `${(stats.bid_levels || 0) + (stats.ask_levels || 0)}`;
|
||||
document.getElementById('spread').textContent = `${(stats.spread_bps || 0).toFixed(2)} bps`;
|
||||
}
|
||||
|
||||
function updateStatus(message, connected) {
|
||||
const statusEl = document.getElementById('status');
|
||||
statusEl.textContent = message;
|
||||
statusEl.className = `status ${connected ? 'connected' : 'disconnected'}`;
|
||||
}
|
||||
|
||||
function toggleSymbol() {
|
||||
currentSymbol = currentSymbol === 'BTC/USDT' ? 'ETH/USDT' : 'BTC/USDT';
|
||||
refreshData();
|
||||
}
|
||||
|
||||
// Auto-refresh every 2 seconds
|
||||
setInterval(refreshData, 2000);
|
||||
|
||||
// Initial load
|
||||
refreshData();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.end_headers()
|
||||
self.wfile.write(html_content.encode())
|
||||
|
||||
def serve_cob_data(self):
|
||||
"""Serve COB data"""
|
||||
try:
|
||||
# Extract symbol from path
|
||||
symbol = self.path.split('/')[-1].replace('%2F', '/')
|
||||
|
||||
if not self.cob_provider:
|
||||
data = self.get_mock_data(symbol)
|
||||
else:
|
||||
data = self.get_real_data(symbol)
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(data).encode())
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error serving COB data: {e}")
|
||||
self.send_error(500, str(e))
|
||||
|
||||
def serve_status(self):
|
||||
"""Serve status"""
|
||||
status = {
|
||||
'server': 'running',
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'cob_provider': 'active' if self.cob_provider else 'mock'
|
||||
}
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(status).encode())
|
||||
|
||||
def get_real_data(self, symbol):
|
||||
"""Get real data from COB provider"""
|
||||
try:
|
||||
cob_snapshot = self.cob_provider.get_consolidated_orderbook(symbol)
|
||||
if not cob_snapshot:
|
||||
return self.get_mock_data(symbol)
|
||||
|
||||
# Convert to dashboard format
|
||||
bids = []
|
||||
asks = []
|
||||
|
||||
for level in cob_snapshot.consolidated_bids[:20]:
|
||||
bids.append({
|
||||
'price': level.price,
|
||||
'size': level.total_size,
|
||||
'volume': level.total_volume_usd
|
||||
})
|
||||
|
||||
for level in cob_snapshot.consolidated_asks[:20]:
|
||||
asks.append({
|
||||
'price': level.price,
|
||||
'size': level.total_size,
|
||||
'volume': level.total_volume_usd
|
||||
})
|
||||
|
||||
return {
|
||||
'symbol': symbol,
|
||||
'bids': bids,
|
||||
'asks': asks,
|
||||
'stats': {
|
||||
'mid_price': cob_snapshot.volume_weighted_mid,
|
||||
'spread_bps': cob_snapshot.spread_bps,
|
||||
'bid_liquidity': cob_snapshot.total_bid_liquidity,
|
||||
'ask_liquidity': cob_snapshot.total_ask_liquidity,
|
||||
'bid_levels': len(cob_snapshot.consolidated_bids),
|
||||
'ask_levels': len(cob_snapshot.consolidated_asks),
|
||||
'imbalance': cob_snapshot.liquidity_imbalance
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting real data: {e}")
|
||||
return self.get_mock_data(symbol)
|
||||
|
||||
def get_mock_data(self, symbol):
|
||||
"""Get mock data for testing"""
|
||||
base_price = 50000 if 'BTC' in symbol else 3000
|
||||
|
||||
bids = []
|
||||
asks = []
|
||||
|
||||
# Generate mock bids
|
||||
for i in range(20):
|
||||
price = base_price - (i * 10)
|
||||
size = 1.0 + (i * 0.1)
|
||||
bids.append({
|
||||
'price': price,
|
||||
'size': size,
|
||||
'volume': price * size
|
||||
})
|
||||
|
||||
# Generate mock asks
|
||||
for i in range(20):
|
||||
price = base_price + 10 + (i * 10)
|
||||
size = 1.0 + (i * 0.1)
|
||||
asks.append({
|
||||
'price': price,
|
||||
'size': size,
|
||||
'volume': price * size
|
||||
})
|
||||
|
||||
return {
|
||||
'symbol': symbol,
|
||||
'bids': bids,
|
||||
'asks': asks,
|
||||
'stats': {
|
||||
'mid_price': base_price + 5,
|
||||
'spread_bps': 2.5,
|
||||
'bid_liquidity': sum(b['volume'] for b in bids),
|
||||
'ask_liquidity': sum(a['volume'] for a in asks),
|
||||
'bid_levels': len(bids),
|
||||
'ask_levels': len(asks),
|
||||
'imbalance': 0.1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
"""Thread pool server"""
|
||||
allow_reuse_address = True
|
||||
|
||||
|
||||
def start_cob_dashboard():
|
||||
"""Start the COB dashboard"""
|
||||
print("Starting Simple COB Dashboard...")
|
||||
|
||||
# Initialize COB provider
|
||||
cob_provider = None
|
||||
try:
|
||||
print("Initializing COB provider...")
|
||||
cob_provider = MultiExchangeCOBProvider(symbols=['BTC/USDT', 'ETH/USDT'])
|
||||
|
||||
# Start in background thread
|
||||
def run_provider():
|
||||
asyncio.run(cob_provider.start_streaming())
|
||||
|
||||
provider_thread = threading.Thread(target=run_provider, daemon=True)
|
||||
provider_thread.start()
|
||||
|
||||
time.sleep(2) # Give it time to connect
|
||||
print("COB provider started")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: COB provider failed to start: {e}")
|
||||
print("Running in mock mode...")
|
||||
|
||||
# Start HTTP server
|
||||
def handler(*args, **kwargs):
|
||||
COBHandler(*args, cob_provider=cob_provider, **kwargs)
|
||||
|
||||
port = 8053
|
||||
server = ThreadedHTTPServer(('localhost', port), handler)
|
||||
|
||||
print(f"COB Dashboard running at http://localhost:{port}")
|
||||
print("Press Ctrl+C to stop")
|
||||
|
||||
# Open browser
|
||||
try:
|
||||
webbrowser.open(f'http://localhost:{port}')
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopping dashboard...")
|
||||
server.shutdown()
|
||||
if cob_provider:
|
||||
asyncio.run(cob_provider.stop_streaming())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
start_cob_dashboard()
|
Reference in New Issue
Block a user