BizzleBot f13e1679cd 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>
2026-03-19 21:36:29 +00:00

496 lines
17 KiB
Python

#!/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 = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BTC ML Optimizer</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{--bg:#0f172a;--card:#1e293b;--card-hover:#253349;--text:#e2e8f0;--text-dim:#94a3b8;--accent:#f7931a;--green:#22c55e;--red:#ef4444;--yellow:#eab308;--border:#334155;--mono:'JetBrains Mono','Fira Code','Courier New',monospace}
body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
.container{max-width:1400px;margin:0 auto;padding:16px}
h1{font-size:1.5rem;font-weight:700;display:flex;align-items:center;gap:10px}
h1 .btc{color:var(--accent);font-size:1.8rem}
h2{font-size:1rem;font-weight:600;color:var(--text-dim);margin-bottom:12px;text-transform:uppercase;letter-spacing:.05em;font-size:.8rem}
/* Header */
.header{display:flex;justify-content:space-between;align-items:center;padding:16px 0;border-bottom:1px solid var(--border);margin-bottom:16px;flex-wrap:wrap;gap:12px}
.controls{display:flex;gap:8px;align-items:center}
.btn{padding:8px 18px;border:none;border-radius:6px;font-family:inherit;font-weight:600;font-size:.85rem;cursor:pointer;transition:all .15s}
.btn-start{background:var(--green);color:#000}.btn-start:hover{background:#16a34a}
.btn-stop{background:var(--red);color:#fff}.btn-stop:hover{background:#dc2626}
.btn-secondary{background:var(--border);color:var(--text)}.btn-secondary:hover{background:var(--card-hover)}
.btn:disabled{opacity:.4;cursor:not-allowed}
/* Status badge */
.status-badge{display:inline-flex;align-items:center;gap:6px;padding:4px 12px;border-radius:20px;font-size:.8rem;font-weight:600}
.status-idle{background:#1e3a5f;color:#60a5fa}
.status-running{background:#1a3a2a;color:var(--green)}
.status-completed{background:#3a2a1a;color:var(--accent)}
.status-error{background:#3a1a1a;color:var(--red)}
.pulse{width:8px;height:8px;border-radius:50%;background:currentColor;animation:pulse 1.5s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
/* Best Sharpe display */
.best-sharpe{text-align:right}
.best-sharpe .label{font-size:.7rem;text-transform:uppercase;letter-spacing:.1em;color:var(--text-dim)}
.best-sharpe .value{font-size:2.2rem;font-weight:700;color:var(--accent);font-family:var(--mono)}
/* Grid layout */
.grid{display:grid;grid-template-columns:1fr 360px;gap:16px}
@media(max-width:900px){.grid{grid-template-columns:1fr}}
/* Cards */
.card{background:var(--card);border-radius:10px;padding:16px;border:1px solid var(--border)}
/* Iteration table */
.table-wrap{overflow-x:auto;max-height:400px;overflow-y:auto}
table{width:100%;border-collapse:collapse;font-size:.82rem}
th{position:sticky;top:0;background:var(--card);text-align:left;padding:8px 10px;color:var(--text-dim);font-weight:600;border-bottom:2px solid var(--border);font-size:.75rem;text-transform:uppercase;letter-spacing:.04em}
td{padding:7px 10px;border-bottom:1px solid var(--border);font-family:var(--mono);font-size:.8rem}
tr.best-row{background:rgba(34,197,94,.1)}
tr.best-row td:first-child{border-left:3px solid var(--green)}
tr:hover{background:var(--card-hover)}
/* Chart */
.chart-container{position:relative;height:260px}
/* LLM panel */
.llm-panel{max-height:500px;overflow-y:auto}
.llm-entry{padding:10px;border-bottom:1px solid var(--border);font-size:.82rem;line-height:1.5}
.llm-entry .iter-label{font-weight:600;color:var(--accent);font-size:.75rem;margin-bottom:4px}
/* Config editor */
.config-section{margin-top:16px}
.config-toggle{cursor:pointer;user-select:none;display:flex;align-items:center;gap:6px}
.config-toggle .arrow{transition:transform .2s;font-size:.7rem}
.config-toggle.open .arrow{transform:rotate(90deg)}
.config-body{display:none;margin-top:12px}
.config-body.open{display:block}
textarea.config-editor{width:100%;height:300px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:12px;font-family:var(--mono);font-size:.8rem;resize:vertical}
.config-actions{display:flex;gap:8px;margin-top:8px}
/* Downloads */
.downloads{display:flex;gap:8px;margin-top:16px;flex-wrap:wrap}
.downloads a{color:var(--accent);text-decoration:none;font-size:.82rem;padding:6px 12px;border:1px solid var(--accent);border-radius:6px;transition:all .15s}
.downloads a:hover{background:var(--accent);color:#000}
/* Footer */
.footer{text-align:center;color:var(--text-dim);font-size:.75rem;padding:20px 0;margin-top:16px;border-top:1px solid var(--border)}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<div>
<h1><span class="btc">&#x20BF;</span> ML Strategy Optimizer</h1>
<div style="margin-top:8px">
<span id="statusBadge" class="status-badge status-idle"><span class="pulse"></span> Idle</span>
</div>
</div>
<div style="display:flex;align-items:center;gap:20px;flex-wrap:wrap">
<div class="controls">
<button id="btnStart" class="btn btn-start" onclick="startOpt()">Start Optimization</button>
<button id="btnStop" class="btn btn-stop" onclick="stopOpt()" disabled>Stop</button>
</div>
<div class="best-sharpe">
<div class="label">Best Sharpe Ratio</div>
<div class="value" id="bestSharpe">0.000</div>
</div>
</div>
</div>
<!-- Main grid -->
<div class="grid">
<div class="left">
<!-- Iteration Table -->
<div class="card" style="margin-bottom:16px">
<h2>Iterations</h2>
<div class="table-wrap">
<table>
<thead>
<tr><th>#</th><th>Sharpe</th><th>Return%</th><th>MaxDD%</th><th>WinRate</th><th>Trades</th><th>PF</th><th>Model</th></tr>
</thead>
<tbody id="iterBody"></tbody>
</table>
</div>
</div>
<!-- Equity Curve Chart -->
<div class="card">
<h2>Performance Over Iterations</h2>
<div class="chart-container">
<canvas id="sharpeChart"></canvas>
</div>
</div>
</div>
<div class="right">
<!-- LLM Analysis -->
<div class="card" style="margin-bottom:16px">
<h2>LLM Analysis</h2>
<div class="llm-panel" id="llmPanel">
<div style="color:var(--text-dim);font-size:.82rem;padding:10px">No suggestions yet.</div>
</div>
</div>
<!-- Downloads -->
<div class="card">
<h2>Downloads</h2>
<div class="downloads">
<a href="/api/download/iterations">iterations.jsonl</a>
<a href="/api/download/best-config">best_config.json</a>
</div>
</div>
</div>
</div>
<!-- Config Editor (collapsible) -->
<div class="card config-section">
<div class="config-toggle" id="configToggle" onclick="toggleConfig()">
<span class="arrow">&#9654;</span>
<h2 style="margin:0">Configuration Editor</h2>
</div>
<div class="config-body" id="configBody">
<textarea class="config-editor" id="configEditor"></textarea>
<div class="config-actions">
<button class="btn btn-start" onclick="updateConfig()">Update Config</button>
<button class="btn btn-secondary" onclick="resetConfig()">Reset to Default</button>
</div>
</div>
</div>
<div class="footer">BTC ML Trading Strategy Optimizer &mdash; VPS &rarr; Windows GPU &rarr; Mac Mini LLM</div>
</div>
<script>
let chart = null;
let pollInterval = null;
// Init chart
function initChart() {
const ctx = document.getElementById('sharpeChart').getContext('2d');
chart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Sharpe Ratio',
data: [],
borderColor: '#f7931a',
backgroundColor: 'rgba(247,147,26,0.1)',
borderWidth: 2,
fill: true,
tension: 0.3,
pointRadius: 4,
pointBackgroundColor: '#f7931a'
}, {
label: 'Return %',
data: [],
borderColor: '#22c55e',
borderWidth: 1.5,
fill: false,
tension: 0.3,
pointRadius: 3,
pointBackgroundColor: '#22c55e',
yAxisID: 'y1'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { intersect: false, mode: 'index' },
plugins: {
legend: { labels: { color: '#94a3b8', font: { size: 11 } } }
},
scales: {
x: { ticks: { color: '#94a3b8' }, grid: { color: '#1e293b' } },
y: { position: 'left', ticks: { color: '#f7931a' }, grid: { color: '#1e293b' }, title: { display: true, text: 'Sharpe', color: '#f7931a' } },
y1: { position: 'right', ticks: { color: '#22c55e' }, grid: { drawOnChartArea: false }, title: { display: true, text: 'Return %', color: '#22c55e' } }
}
}
});
}
function updateStatusBadge(status) {
const badge = document.getElementById('statusBadge');
const state = status.state || 'idle';
badge.className = 'status-badge status-' + state;
let text = state.charAt(0).toUpperCase() + state.slice(1);
if (state === 'running' && status.iteration > 0) {
text += ' (iteration ' + status.iteration + '/' + status.max_iterations + ')';
}
if (state === 'error' && status.error) {
text += ': ' + status.error.substring(0, 60);
}
badge.innerHTML = '<span class="pulse"></span> ' + text;
document.getElementById('btnStart').disabled = (state === 'running');
document.getElementById('btnStop').disabled = (state !== 'running');
document.getElementById('bestSharpe').textContent = (status.best_sharpe || 0).toFixed(3);
}
function updateIterations(iterations) {
const tbody = document.getElementById('iterBody');
if (!iterations.length) {
tbody.innerHTML = '<tr><td colspan="8" style="color:var(--text-dim);text-align:center">No iterations yet</td></tr>';
return;
}
const bestSharpe = Math.max(...iterations.map(i => i.sharpe || 0));
let html = '';
for (const it of iterations) {
const isBest = it.sharpe === bestSharpe && bestSharpe > 0;
const sc = it.sharpe > 1.5 ? 'var(--green)' : it.sharpe > 1.0 ? 'var(--yellow)' : 'var(--red)';
html += '<tr class="' + (isBest ? 'best-row' : '') + '">';
html += '<td>' + it.iteration + '</td>';
html += '<td style="color:' + sc + ';font-weight:600">' + (it.sharpe||0).toFixed(3) + '</td>';
html += '<td>' + (it["return"]||0).toFixed(1) + '</td>';
html += '<td>' + (it.max_drawdown||0).toFixed(1) + '</td>';
html += '<td>' + ((it.win_rate||0)*100).toFixed(1) + '%</td>';
html += '<td>' + (it.trades||0) + '</td>';
html += '<td>' + (it.profit_factor||0).toFixed(2) + '</td>';
html += '<td>' + (it.model_type||'') + '</td>';
html += '</tr>';
}
tbody.innerHTML = html;
// auto-scroll to bottom
const wrap = tbody.closest('.table-wrap');
wrap.scrollTop = wrap.scrollHeight;
}
function updateChart(iterations) {
if (!chart || !iterations.length) return;
chart.data.labels = iterations.map(i => '#' + i.iteration);
chart.data.datasets[0].data = iterations.map(i => i.sharpe || 0);
chart.data.datasets[1].data = iterations.map(i => i["return"] || 0);
chart.update('none');
}
function updateLLM(suggestions) {
const panel = document.getElementById('llmPanel');
if (!suggestions || !suggestions.length) return;
let html = '';
for (const s of suggestions.slice().reverse()) {
html += '<div class="llm-entry"><div class="iter-label">Iteration ' + s.iteration + '</div>' + escapeHtml(s.reasoning) + '</div>';
}
panel.innerHTML = html;
}
function escapeHtml(text) {
const d = document.createElement('div');
d.textContent = text || '';
return d.innerHTML;
}
async function poll() {
try {
const [statusRes, iterRes] = await Promise.all([
fetch('/api/status'), fetch('/api/iterations')
]);
const status = await statusRes.json();
const iterations = await iterRes.json();
updateStatusBadge(status);
updateIterations(iterations);
updateChart(iterations);
updateLLM(status.llm_suggestions || []);
} catch(e) { console.error('Poll error:', e); }
}
async function startOpt() {
try {
const r = await fetch('/api/start', { method: 'POST' });
const d = await r.json();
if (d.error) alert(d.error);
} catch(e) { alert('Failed: ' + e); }
setTimeout(poll, 500);
}
async function stopOpt() {
try { await fetch('/api/stop', { method: 'POST' }); } catch(e) { alert('Failed: ' + e); }
setTimeout(poll, 500);
}
function toggleConfig() {
const toggle = document.getElementById('configToggle');
const body = document.getElementById('configBody');
toggle.classList.toggle('open');
body.classList.toggle('open');
if (body.classList.contains('open')) loadConfig();
}
async function loadConfig() {
try {
const r = await fetch('/api/config');
const c = await r.json();
document.getElementById('configEditor').value = JSON.stringify(c, null, 2);
} catch(e) { console.error(e); }
}
async function updateConfig() {
try {
const text = document.getElementById('configEditor').value;
const config = JSON.parse(text);
const r = await fetch('/api/config', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ config })
});
const d = await r.json();
if (d.ok) alert('Config updated!');
} catch(e) { alert('Invalid JSON or error: ' + e); }
}
async function resetConfig() {
if (!confirm('Reset to initial config?')) return;
try {
const r = await fetch('/api/config');
// Fetch the initial config by reading it — for now just reload
location.reload();
} catch(e) { alert(e); }
}
// Init
initChart();
poll();
pollInterval = setInterval(poll, 10000);
</script>
</body>
</html>"""
@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)