From f13e1679cd8efcb6d8de8a9b37d1b94693158b5c Mon Sep 17 00:00:00 2001 From: BizzleBot Date: Thu, 19 Mar 2026 21:36:29 +0000 Subject: [PATCH] feat: add web dashboard for BTC ML optimizer FastAPI dashboard on port 3088 with live iteration tracking, Sharpe ratio chart, LLM analysis panel, config editor, and download links. Orchestrator refactored to support library usage with run_optimization_loop(), stop_flag, and callbacks. Co-Authored-By: Claude Opus 4.6 --- dashboard/server.py | 495 +++++++++++++++++++++++++++++++++++++++++++ orchestrator.py | 168 +++++++++++++++ requirements_vps.txt | 2 + 3 files changed, 665 insertions(+) create mode 100644 dashboard/server.py 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 = """ + + + + +BTC ML Optimizer + + + + + + +
+ + +
+
+

ML Strategy Optimizer

+
+ Idle +
+
+
+
+ + +
+
+
Best Sharpe Ratio
+
0.000
+
+
+
+ + +
+
+ +
+

Iterations

+
+ + + + + +
#SharpeReturn%MaxDD%WinRateTradesPFModel
+
+
+ +
+

Performance Over Iterations

+
+ +
+
+
+ +
+ +
+

LLM Analysis

+
+
No suggestions yet.
+
+
+ + +
+

Downloads

+ +
+
+
+ + +
+
+ +

Configuration Editor

+
+
+ +
+ + +
+
+
+ + +
+ + + +""" + + +@app.get("/", response_class=HTMLResponse) +def dashboard(): + return DASHBOARD_HTML + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=3088) diff --git a/orchestrator.py b/orchestrator.py index 9d9254d..b264ea0 100755 --- a/orchestrator.py +++ b/orchestrator.py @@ -8,6 +8,7 @@ import json import os import subprocess import sys +import threading import time from datetime import datetime, timezone @@ -323,5 +324,172 @@ def main(): """) +# --- Library API for dashboard integration --- + +# Shared state for dashboard +_stop_event = threading.Event() +_status = { + "state": "idle", # idle, running, completed, error + "iteration": 0, + "max_iterations": MAX_ITERATIONS, + "best_sharpe": 0.0, + "error": None, + "llm_suggestions": [], # list of {iteration, reasoning, changes} +} +_status_lock = threading.Lock() + + +def get_status(): + """Get current optimization status (thread-safe).""" + with _status_lock: + return dict(_status) + + +def update_status(**kwargs): + """Update status fields (thread-safe).""" + with _status_lock: + _status.update(kwargs) + + +def run_optimization_loop(callback=None, config_override=None): + """ + Run the optimization loop. Designed to be called from a background thread. + + Args: + callback: Called after each iteration with (iteration_number, iter_data_dict). + config_override: Optional dict to use instead of loading from disk. + """ + _stop_event.clear() + update_status(state="running", iteration=0, error=None, best_sharpe=0.0) + + try: + os.makedirs(RESULTS_DIR, exist_ok=True) + ensure_data() + + config_path = os.path.join(CONFIG_DIR, "initial_config.json") + best_config_path = os.path.join(CONFIG_DIR, "best_config.json") + + if config_override: + config = config_override + elif os.path.exists(best_config_path): + with open(best_config_path) as f: + config = json.load(f) + else: + with open(config_path) as f: + config = json.load(f) + + history = load_iteration_history() + start_iter = len(history) + 1 + best_sharpe = max((h["sharpe"] for h in history), default=0) + update_status(best_sharpe=best_sharpe) + + setup_windows_remote() + scp_to_windows(os.path.join(BASE_DIR, "ml_engine", "train_and_backtest.py"), "train_and_backtest.py") + for tf in ["1h", "4h"]: + data_file = os.path.join(DATA_DIR, f"btc_{tf}.csv") + if os.path.exists(data_file): + scp_to_windows(data_file, f"btc_{tf}.csv") + + sys.path.insert(0, os.path.join(BASE_DIR, "llm_client")) + from analyzer import analyze_and_suggest + + for iteration in range(start_iter, MAX_ITERATIONS + 1): + if _stop_event.is_set(): + update_status(state="completed") + return + update_status(iteration=iteration) + + tmp_config = os.path.join(BASE_DIR, "config", "current_config.json") + with open(tmp_config, "w") as f: + json.dump(config, f, indent=2) + scp_to_windows(tmp_config, "config.json") + + try: + run_ml_training() + except (RuntimeError, subprocess.TimeoutExpired) as e: + if callback: + callback(iteration, {"error": str(e)}) + if history: + config = history[-1].get("config", config) + continue + + results_local = os.path.join(RESULTS_DIR, f"results_iter_{iteration}.json") + scp_from_windows("results.json", results_local) + with open(results_local) as f: + results = json.load(f) + + current_sharpe = results.get("sharpe_ratio", 0) + is_best = current_sharpe > best_sharpe + if is_best: + best_sharpe = current_sharpe + with open(best_config_path, "w") as f: + json.dump(config, f, indent=2) + update_status(best_sharpe=best_sharpe) + + iter_data = { + "iteration": iteration, + "timestamp": datetime.now(timezone.utc).isoformat(), + "sharpe": current_sharpe, + "return": results.get("total_return_pct", 0), + "max_drawdown": results.get("max_drawdown_pct", 0), + "win_rate": results.get("win_rate", 0), + "trades": results.get("trade_count", 0), + "profit_factor": results.get("profit_factor", 0), + "model_type": config.get("model_type", "unknown"), + "is_best": is_best, + "config": config, + "results": results, + } + save_iteration(iter_data) + history.append(iter_data) + + if callback: + callback(iteration, iter_data) + + converged, reason = check_convergence(history) + if converged: + update_status(state="completed") + return + + if iteration >= MAX_ITERATIONS: + update_status(state="completed") + return + + if _stop_event.is_set(): + update_status(state="completed") + return + + # LLM suggestion + try: + summary_history = [ + {k: h[k] for k in ("iteration", "sharpe", "return", "win_rate", "trades", "model_type")} + for h in history + ] + new_config, reasoning = analyze_and_suggest(config, results, summary_history) + with _status_lock: + _status["llm_suggestions"].append({ + "iteration": iteration, + "reasoning": reasoning, + }) + config = new_config + except Exception: + import random + hp = config.get("hyperparameters", {}) + hp["learning_rate"] = hp.get("learning_rate", 0.05) * random.uniform(0.8, 1.2) + hp["max_depth"] = max(3, min(10, hp.get("max_depth", 6) + random.choice([-1, 0, 1]))) + config["hyperparameters"] = hp + + update_status(state="completed") + + except Exception as e: + update_status(state="error", error=str(e)) + raise + + +def request_stop(): + """Request graceful stop of the optimization loop.""" + _stop_event.set() + + if __name__ == "__main__": main() diff --git a/requirements_vps.txt b/requirements_vps.txt index 46db4ce..978a35c 100644 --- a/requirements_vps.txt +++ b/requirements_vps.txt @@ -1,2 +1,4 @@ ccxt requests +fastapi +uvicorn