""" Dashboard backend — WebSocket metrics server. Collects strategy performance data in real time and streams it to connected dashboard clients via WebSocket. Architecture: - FastAPI serves the WebSocket endpoint at /ws - A background thread collects metrics at 1-second intervals - Connected clients receive JSON updates with PnL, positions, and trade history for all strategies - Serves static dashboard HTML at / Usage: python dashboard/server.py --port 9175 """ import asyncio import json import time import threading from dataclasses import dataclass, field, asdict from pathlib import Path from typing import Optional from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse import uvicorn # ═══════════════════════════════════════════════════════════════ # Data Models # ═══════════════════════════════════════════════════════════════ @dataclass class StrategyMetrics: """Real-time metrics for a single strategy.""" name: str pnl: float = 0.0 pnl_pct: float = 0.0 position: float = 0.0 trades_today: int = 0 win_rate: float = 0.0 sharpe: float = 0.0 max_drawdown: float = 0.0 status: str = "idle" # idle, running, error @dataclass class DashboardState: """Complete dashboard state broadcast to clients.""" timestamp: float = 0.0 total_pnl: float = 0.0 total_equity: float = 10000.0 strategies: dict[str, StrategyMetrics] = field(default_factory=dict) equity_history: list[dict] = field(default_factory=list) trades: list[dict] = field(default_factory=list) # ═══════════════════════════════════════════════════════════════ # Globals # ═══════════════════════════════════════════════════════════════ app = FastAPI(title="FTDT Quant Lab Dashboard") state = DashboardState() connected_clients: set[WebSocket] = set() state_lock = threading.Lock() # Initialize strategy placeholders STRATEGY_NAMES = [ "Order Book Imbalance", "Iceberg Detection", "Funding Rate Arb", "Pairs Trading", "Avellaneda-Stoikov", ] for name in STRATEGY_NAMES: state.strategies[name] = StrategyMetrics(name=name) # ═══════════════════════════════════════════════════════════════ # Metrics collector (mock — replace with real NautilusTrader hooks) # ═══════════════════════════════════════════════════════════════ def collect_metrics(): """ Background thread that updates the dashboard state. In production, this would read from NautilusTrader's portfolio and risk engine. For now, it generates demo data so the dashboard shows something meaningful. """ import random import math t = 0 while True: time.sleep(1) t += 1 with state_lock: state.timestamp = time.time() # Simulate some PnL movement for i, name in enumerate(STRATEGY_NAMES): s = state.strategies[name] # Each strategy has different behavior if name == "Order Book Imbalance": s.pnl += random.gauss(0.02, 0.5) s.trades_today = int(t / 30) s.win_rate = 0.52 + random.gauss(0, 0.02) elif name == "Iceberg Detection": s.pnl += random.gauss(0.01, 0.3) s.trades_today = int(t / 60) s.win_rate = 0.48 + random.gauss(0, 0.03) elif name == "Funding Rate Arb": s.pnl += 0.001 # Steady carry s.trades_today = 1 s.win_rate = 0.99 elif name == "Pairs Trading": s.pnl += random.gauss(0.0, 0.4) s.trades_today = int(t / 45) s.win_rate = 0.55 + random.gauss(0, 0.02) elif name == "Avellaneda-Stoikov": s.pnl += random.gauss(0.03, 0.2) s.trades_today = int(t / 10) s.win_rate = 0.60 + random.gauss(0, 0.01) s.pnl_pct = (s.pnl / state.total_equity) * 100 s.position = s.pnl * random.uniform(0.1, 0.5) s.sharpe = 0.5 + random.gauss(0, 0.1) s.max_drawdown = abs(s.pnl) * 0.3 if s.pnl < 0 else 0.0 s.status = "running" state.total_pnl = sum(s.pnl for s in state.strategies.values()) # Keep equity history (last 200 points) state.equity_history.append({ "t": state.timestamp, "v": state.total_equity + state.total_pnl, }) if len(state.equity_history) > 200: state.equity_history = state.equity_history[-200:] # Add trade if significant PnL move if abs(state.total_pnl) % 0.5 < 0.01 and len(state.trades) < 50: state.trades.append({ "time": time.strftime("%H:%M:%S"), "strategy": random.choice(STRATEGY_NAMES), "side": random.choice(["BUY", "SELL"]), "size": round(random.uniform(0.001, 0.01), 4), "pnl": round(random.gauss(0.1, 0.5), 4), }) # Broadcast to all connected clients payload = json.dumps(asdict(state), default=str) # We need to run this in the event loop for ws in list(connected_clients): try: asyncio.run_coroutine_threadsafe( ws.send_text(payload), loop ) except Exception: connected_clients.discard(ws) # ═══════════════════════════════════════════════════════════════ # WebSocket endpoint # ═══════════════════════════════════════════════════════════════ @app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket): await websocket.accept() connected_clients.add(websocket) try: while True: # Keep alive — actual data is pushed by the collector thread await asyncio.sleep(30) except WebSocketDisconnect: connected_clients.discard(websocket) # ═══════════════════════════════════════════════════════════════ # Static files # ═══════════════════════════════════════════════════════════════ STATIC_DIR = Path(__file__).parent / "static" @app.get("/") async def root(): return FileResponse(STATIC_DIR / "index.html") # ═══════════════════════════════════════════════════════════════ # Main # ═══════════════════════════════════════════════════════════════ loop: asyncio.AbstractEventLoop = None def main(): import argparse parser = argparse.ArgumentParser() parser.add_argument("--port", type=int, default=9175) parser.add_argument("--host", default="127.0.0.1") args = parser.parse_args() global loop loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) # Start metrics collector in background collector = threading.Thread(target=collect_metrics, daemon=True) collector.start() # Mount static files app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") print(f"FTDT Quant Lab Dashboard") print(f" http://{args.host}:{args.port}") print(f" WebSocket: ws://{args.host}:{args.port}/ws") uvicorn.run(app, host=args.host, port=args.port, log_level="warning") if __name__ == "__main__": main()