pivot: rewrite as BTC accumulation signal optimizer
Replace day-trading bot with long-term accumulation signal model. Predicts optimal BUY times using forward return analysis at 7d/30d/90d horizons, scoring each candle 0-100. Primary metric is now cost_basis_improvement_pct (model buy price vs DCA). - train_and_backtest.py: regression models (XGBoost/LSTM hybrid), accumulation-focused features (price position, momentum, volatility, volume, cycle), forward return targets, signal quality backtesting - orchestrator.py: cost improvement scoring, signal count validation - analyzer.py: accumulation-focused LLM system prompt - dashboard: cost improvement display, signal metrics table - config: new accumulation-focused parameters Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a21e635d9f
commit
560863fa0d
+39
-74
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
BTC ML Trading Strategy Optimizer — Web Dashboard
|
||||
BTC Accumulation Signal Optimizer -- Web Dashboard
|
||||
FastAPI server with inline HTML/CSS/JS dashboard.
|
||||
"""
|
||||
|
||||
@@ -13,42 +13,35 @@ 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")
|
||||
app = FastAPI(title="BTC Accumulation Signal Optimizer")
|
||||
|
||||
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
|
||||
_opt_thread = None
|
||||
|
||||
|
||||
class ConfigUpdate(BaseModel):
|
||||
config: dict
|
||||
|
||||
|
||||
# ── API Endpoints ──────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.get("/api/status")
|
||||
def api_status():
|
||||
status = orchestrator.get_status()
|
||||
return status
|
||||
return orchestrator.get_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"}
|
||||
entry = {k: v for k, v in it.items() if k not in ("config", "results")}
|
||||
slim.append(entry)
|
||||
return slim
|
||||
|
||||
@@ -94,11 +87,11 @@ def api_stop():
|
||||
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}
|
||||
return {"config": None, "best_score": 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 {}
|
||||
best_iter = max(iterations, key=lambda x: x.get("cost_improvement", 0)) if iterations else {}
|
||||
return {"config": config, "best_iteration": best_iter}
|
||||
|
||||
|
||||
@@ -117,15 +110,12 @@ def api_download_best_config():
|
||||
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>
|
||||
<title>BTC Accumulation Signal 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>
|
||||
@@ -138,7 +128,6 @@ 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}
|
||||
@@ -147,7 +136,6 @@ h2{font-size:1rem;font-weight:600;color:var(--text-dim);margin-bottom:12px;text-
|
||||
.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)}
|
||||
@@ -156,19 +144,16 @@ h2{font-size:1rem;font-weight:600;color:var(--text-dim);margin-bottom:12px;text-
|
||||
.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)}
|
||||
.best-score{text-align:right}
|
||||
.best-score .label{font-size:.7rem;text-transform:uppercase;letter-spacing:.1em;color:var(--text-dim)}
|
||||
.best-score .value{font-size:2.2rem;font-weight:700;color:var(--accent);font-family:var(--mono)}
|
||||
.best-score .unit{font-size:1rem;color:var(--text-dim)}
|
||||
|
||||
/* 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}
|
||||
@@ -177,15 +162,12 @@ 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}
|
||||
@@ -195,22 +177,19 @@ tr:hover{background:var(--card-hover)}
|
||||
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">₿</span> ML Strategy Optimizer</h1>
|
||||
<h1><span class="btc">₿</span> Accumulation Signal Optimizer</h1>
|
||||
<div style="margin-top:8px">
|
||||
<span id="statusBadge" class="status-badge status-idle"><span class="pulse"></span> Idle</span>
|
||||
</div>
|
||||
@@ -220,47 +199,41 @@ textarea.config-editor{width:100%;height:300px;background:var(--bg);color:var(--
|
||||
<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 class="best-score">
|
||||
<div class="label">Best Cost Improvement</div>
|
||||
<div class="value" id="bestScore">0.0<span class="unit">%</span></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>
|
||||
<tr><th>#</th><th>Cost Imp%</th><th>Signals</th><th>Frequency</th><th>R2</th><th>Bottoms</th><th>Tops</th><th>Model</th></tr>
|
||||
</thead>
|
||||
<tbody id="iterBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Equity Curve Chart -->
|
||||
<div class="card">
|
||||
<h2>Performance Over Iterations</h2>
|
||||
<h2>Cost Improvement Over Iterations</h2>
|
||||
<div class="chart-container">
|
||||
<canvas id="sharpeChart"></canvas>
|
||||
<canvas id="mainChart"></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">
|
||||
@@ -271,7 +244,6 @@ textarea.config-editor{width:100%;height:300px;background:var(--bg);color:var(--
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Config Editor (collapsible) -->
|
||||
<div class="card config-section">
|
||||
<div class="config-toggle" id="configToggle" onclick="toggleConfig()">
|
||||
<span class="arrow">▶</span>
|
||||
@@ -286,22 +258,21 @@ textarea.config-editor{width:100%;height:300px;background:var(--bg);color:var(--
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">BTC ML Trading Strategy Optimizer — VPS → Windows GPU → Mac Mini LLM</div>
|
||||
<div class="footer">BTC Accumulation Signal Optimizer — VPS → Windows GPU → Mac Mini LLM</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let chart = null;
|
||||
let pollInterval = null;
|
||||
|
||||
// Init chart
|
||||
function initChart() {
|
||||
const ctx = document.getElementById('sharpeChart').getContext('2d');
|
||||
const ctx = document.getElementById('mainChart').getContext('2d');
|
||||
chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'Sharpe Ratio',
|
||||
label: 'Cost Improvement %',
|
||||
data: [],
|
||||
borderColor: '#f7931a',
|
||||
backgroundColor: 'rgba(247,147,26,0.1)',
|
||||
@@ -311,7 +282,7 @@ function initChart() {
|
||||
pointRadius: 4,
|
||||
pointBackgroundColor: '#f7931a'
|
||||
}, {
|
||||
label: 'Return %',
|
||||
label: 'Signal Count',
|
||||
data: [],
|
||||
borderColor: '#22c55e',
|
||||
borderWidth: 1.5,
|
||||
@@ -331,8 +302,8 @@ function initChart() {
|
||||
},
|
||||
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' } }
|
||||
y: { position: 'left', ticks: { color: '#f7931a' }, grid: { color: '#1e293b' }, title: { display: true, text: 'Cost Improvement %', color: '#f7931a' } },
|
||||
y1: { position: 'right', ticks: { color: '#22c55e' }, grid: { drawOnChartArea: false }, title: { display: true, text: 'Signal Count', color: '#22c55e' } }
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -353,7 +324,7 @@ function updateStatusBadge(status) {
|
||||
|
||||
document.getElementById('btnStart').disabled = (state === 'running');
|
||||
document.getElementById('btnStop').disabled = (state !== 'running');
|
||||
document.getElementById('bestSharpe').textContent = (status.best_sharpe || 0).toFixed(3);
|
||||
document.getElementById('bestScore').innerHTML = (status.best_score || 0).toFixed(1) + '<span class="unit">%</span>';
|
||||
}
|
||||
|
||||
function updateIterations(iterations) {
|
||||
@@ -362,24 +333,23 @@ function updateIterations(iterations) {
|
||||
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));
|
||||
const bestCI = Math.max(...iterations.map(i => i.cost_improvement || 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)';
|
||||
const isBest = it.cost_improvement === bestCI && bestCI > 0;
|
||||
const sc = it.cost_improvement > 15 ? 'var(--green)' : it.cost_improvement > 10 ? '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 += '<td style="color:' + sc + ';font-weight:600">' + (it.cost_improvement||0).toFixed(1) + '</td>';
|
||||
html += '<td>' + (it.signal_count||0) + '</td>';
|
||||
html += '<td>' + (it.signal_frequency||0).toFixed(1) + '%</td>';
|
||||
html += '<td>' + (it.r2_score||0).toFixed(4) + '</td>';
|
||||
html += '<td>' + (it.score_at_bottoms||0).toFixed(1) + '</td>';
|
||||
html += '<td>' + (it.score_at_tops||0).toFixed(1) + '</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;
|
||||
}
|
||||
@@ -387,8 +357,8 @@ function updateIterations(iterations) {
|
||||
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.data.datasets[0].data = iterations.map(i => i.cost_improvement || 0);
|
||||
chart.data.datasets[1].data = iterations.map(i => i.signal_count || 0);
|
||||
chart.update('none');
|
||||
}
|
||||
|
||||
@@ -468,14 +438,9 @@ async function updateConfig() {
|
||||
|
||||
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); }
|
||||
try { location.reload(); } catch(e) { alert(e); }
|
||||
}
|
||||
|
||||
// Init
|
||||
initChart();
|
||||
poll();
|
||||
pollInterval = setInterval(poll, 10000);
|
||||
|
||||
Reference in New Issue
Block a user