Verbose dashboard with backtesting tab and per-strategy 100 USDC allocation

Dashboard overhaul:
- Tabbed interface: Live Trading | Backtesting
- Live tab shows: global stats (equity, reserve, trades, win rate, active
  strategies), equity curve, per-strategy cards with allocation and PnL,
  real-time trade log
- Backtest tab: lists saved backtests with Sharpe, PnL, max DD, win rate;
  click to view full equity curve and detailed metrics
- Reads real data from /tmp/ftdt-metrics.json written by live node

Live node update:
- 5 strategies each with 100 USDC allocation (398 USDC reserve)
- Writes real-time metrics to shared JSON file
- Runs signal generators for each strategy type
- Logs tick-by-tick status

Backtest runner:
- Simulates 30 days of hourly data per strategy
- Different return profiles for each strategy type
- Saves results to backtests/results/ as JSON
- Accessible via dashboard API and frontend

Backtest results (30-day sim):
  Avellaneda-Stoikov:    +3.72%  Sharpe 2.53  DD 5.12%
  Order Book Imbalance:  +3.83%  Sharpe 1.60  DD 9.86%
  Pairs Trading:         +0.54%  Sharpe 0.41  DD 7.83%
  Funding Rate Arb:      +0.17%  Sharpe 0.35  DD 2.94%
  Iceberg Detection:     -9.15%  Sharpe -4.39 DD 11.94%
This commit is contained in:
ramseshk
2026-08-04 03:12:21 +00:00
parent bbd309db6b
commit 7dd9e78e0b
9 changed files with 18969 additions and 697 deletions
+121 -147
View File
@@ -1,225 +1,199 @@
"""
Dashboard backend — WebSocket metrics server.
Collects strategy performance data in real time and streams
it to connected dashboard clients via WebSocket.
Reads live metrics from a shared JSON file (written by the live node)
and serves backtest results from disk. Streams everything 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 /
- /ws — WebSocket for real-time streaming
- /backtests — list available backtest results
- /backtest/{name} — serve specific backtest result
- / — static HTML dashboard
Usage:
python dashboard/server.py --port 9175
"""
import asyncio
import json
import os
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
from fastapi.responses import FileResponse, JSONResponse
import uvicorn
# ═══════════════════════════════════════════════════════════
# Constants
# ═══════════════════════════════════════════════════════════
# ═══════════════════════════════════════════════════════════════
# Data Models
# ═══════════════════════════════════════════════════════════════
METRICS_FILE = "/tmp/ftdt-metrics.json"
BACKTEST_DIR = "/home/debian/ftdt-quant-lab/backtests/results"
STATIC_DIR = Path(__file__).parent / "static"
@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
# Ensure backtest dir exists
os.makedirs(BACKTEST_DIR, exist_ok=True)
@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
# ═══════════════════════════════════════════════════════════
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)
loop: Optional[asyncio.AbstractEventLoop] = None
# ═══════════════════════════════════════════════════════════════
# Metrics collector (mock — replace with real NautilusTrader hooks)
# ═══════════════════════════════════════════════════════════════
# ═══════════════════════════════════════════════════════════
# Metrics reader
# ═══════════════════════════════════════════════════════════
def collect_metrics():
"""
Background thread that updates the dashboard state.
def read_metrics() -> dict:
"""Read the shared metrics file written by the live node."""
try:
if os.path.exists(METRICS_FILE):
with open(METRICS_FILE) as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
pass
return _empty_metrics()
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
def _empty_metrics() -> dict:
return {
"timestamp": time.time(),
"wallet": "0x...",
"total_equity": 898.0,
"total_pnl": 0.0,
"total_pnl_pct": 0.0,
"equity_history": [],
"strategies": {},
"trades": [],
"status": "starting",
}
# ═══════════════════════════════════════════════════════════
# Background broadcaster
# ═══════════════════════════════════════════════════════════
async def broadcast_to_client(ws: WebSocket, payload: str):
try:
await ws.send_text(payload)
except Exception:
connected_clients.discard(ws)
def broadcast_loop():
"""Continuously read metrics and broadcast to all clients."""
while True:
time.sleep(1)
t += 1
data = read_metrics()
payload = json.dumps(data, default=str)
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:
if loop:
asyncio.run_coroutine_threadsafe(
ws.send_text(payload), loop
broadcast_to_client(ws, payload), loop
)
except Exception:
connected_clients.discard(ws)
# ═══════════════════════════════════════════════════════════════
# WebSocket endpoint
# ═══════════════════════════════════════════════════════════════
# ═══════════════════════════════════════════════════════════
# WebSocket
# ═══════════════════════════════════════════════════════════
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
connected_clients.add(websocket)
try:
# Send initial state immediately
data = read_metrics()
await websocket.send_text(json.dumps(data, default=str))
while True:
# Keep alive — actual data is pushed by the collector thread
await asyncio.sleep(30)
except WebSocketDisconnect:
connected_clients.discard(websocket)
# ═══════════════════════════════════════════════════════════════
# Static files
# ═══════════════════════════════════════════════════════════════
# ═══════════════════════════════════════════════════════════
# Backtest endpoints
# ═══════════════════════════════════════════════════════════
STATIC_DIR = Path(__file__).parent / "static"
@app.get("/api/backtests")
async def list_backtests():
"""List all saved backtest results."""
results = []
if os.path.isdir(BACKTEST_DIR):
for fname in sorted(os.listdir(BACKTEST_DIR), reverse=True):
if fname.endswith(".json"):
fpath = os.path.join(BACKTEST_DIR, fname)
try:
with open(fpath) as f:
data = json.load(f)
results.append({
"name": fname.replace(".json", ""),
"strategy": data.get("strategy", "unknown"),
"start": data.get("start_time"),
"end": data.get("end_time"),
"sharpe": data.get("sharpe", 0),
"pnl_pct": data.get("pnl_pct", 0),
"max_dd": data.get("max_dd", 0),
"win_rate": data.get("win_rate", 0),
})
except (json.JSONDecodeError, IOError):
pass
return JSONResponse(results)
@app.get("/api/backtest/{name}")
async def get_backtest(name: str):
"""Get full backtest result data."""
fpath = os.path.join(BACKTEST_DIR, f"{name}.json")
if os.path.exists(fpath):
with open(fpath) as f:
return JSONResponse(json.load(f))
return JSONResponse({"error": "not found"}, status_code=404)
# ═══════════════════════════════════════════════════════════
# Static
# ═══════════════════════════════════════════════════════════
@app.get("/")
async def root():
return FileResponse(STATIC_DIR / "index.html")
# ═══════════════════════════════════════════════════════════════
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
# ═══════════════════════════════════════════════════════════
# 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")
parser.add_argument("--host", default="0.0.0.0")
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")
# Start metrics broadcaster
broadcaster = threading.Thread(target=broadcast_loop, daemon=True)
broadcaster.start()
print(f"FTDT Quant Lab Dashboard")
print(f" http://{args.host}:{args.port}")
print(f" WebSocket: ws://{args.host}:{args.port}/ws")
print(f" Backtests: /api/backtests")
uvicorn.run(app, host=args.host, port=args.port, log_level="warning")