401 lines
14 KiB
Python
401 lines
14 KiB
Python
#!/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() |