diff --git a/dashboard/server.py b/dashboard/server.py new file mode 100644 index 0000000..8b77f87 --- /dev/null +++ b/dashboard/server.py @@ -0,0 +1,495 @@ +#!/usr/bin/env python3 +""" +BTC ML Trading Strategy Optimizer — Web Dashboard +FastAPI server with inline HTML/CSS/JS dashboard. +""" + +import json +import os +import sys +import threading + +from fastapi import FastAPI +from fastapi.responses import FileResponse, HTMLResponse, JSONResponse +from pydantic import BaseModel + +# Add project root to path +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, BASE_DIR) + +import orchestrator + +app = FastAPI(title="BTC ML Optimizer Dashboard") + +CONFIG_DIR = os.path.join(BASE_DIR, "config") +RESULTS_DIR = os.path.join(BASE_DIR, "results") +ITERATIONS_LOG = os.path.join(RESULTS_DIR, "iterations.jsonl") + +# Background thread reference +_opt_thread: threading.Thread | None = None + + +class ConfigUpdate(BaseModel): + config: dict + + +# ── API Endpoints ────────────────────────────────────────────── + + +@app.get("/api/status") +def api_status(): + status = orchestrator.get_status() + return status + + +@app.get("/api/iterations") +def api_iterations(): + iterations = orchestrator.load_iteration_history() + # Strip heavy config from list view + slim = [] + for it in iterations: + entry = {k: v for k, v in it.items() if k != "config"} + slim.append(entry) + return slim + + +@app.get("/api/config") +def api_config(): + best = os.path.join(CONFIG_DIR, "best_config.json") + initial = os.path.join(CONFIG_DIR, "initial_config.json") + path = best if os.path.exists(best) else initial + with open(path) as f: + return json.load(f) + + +@app.put("/api/config") +def api_update_config(body: ConfigUpdate): + path = os.path.join(CONFIG_DIR, "current_config.json") + with open(path, "w") as f: + json.dump(body.config, f, indent=2) + return {"ok": True} + + +@app.post("/api/start") +def api_start(): + global _opt_thread + status = orchestrator.get_status() + if status["state"] == "running": + return JSONResponse({"error": "Already running"}, status_code=409) + + _opt_thread = threading.Thread( + target=orchestrator.run_optimization_loop, daemon=True + ) + _opt_thread.start() + return {"ok": True, "message": "Optimization started"} + + +@app.post("/api/stop") +def api_stop(): + orchestrator.request_stop() + return {"ok": True, "message": "Stop requested"} + + +@app.get("/api/best") +def api_best(): + best_path = os.path.join(CONFIG_DIR, "best_config.json") + if not os.path.exists(best_path): + return {"config": None, "sharpe": 0} + with open(best_path) as f: + config = json.load(f) + iterations = orchestrator.load_iteration_history() + best_iter = max(iterations, key=lambda x: x.get("sharpe", 0)) if iterations else {} + return {"config": config, "best_iteration": best_iter} + + +@app.get("/api/download/iterations") +def api_download_iterations(): + if os.path.exists(ITERATIONS_LOG): + return FileResponse(ITERATIONS_LOG, filename="iterations.jsonl") + return JSONResponse({"error": "No iterations yet"}, status_code=404) + + +@app.get("/api/download/best-config") +def api_download_best_config(): + path = os.path.join(CONFIG_DIR, "best_config.json") + if os.path.exists(path): + return FileResponse(path, filename="best_config.json") + return JSONResponse({"error": "No best config yet"}, status_code=404) + + +# ── Dashboard HTML ───────────────────────────────────────────── + + +DASHBOARD_HTML = """ + +
+ + +| # | Sharpe | Return% | MaxDD% | WinRate | Trades | PF | Model |
|---|