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 <noreply@anthropic.com>
This commit is contained in:
BizzleBot
2026-03-19 21:36:29 +00:00
co-authored by Claude Opus 4.6
parent 8ff35c1a86
commit f13e1679cd
3 changed files with 665 additions and 0 deletions
+168
View File
@@ -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()