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