<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stock Prediction Engine</title>
<script src="https://cdn.plot.ly/plotly-2.32.0.min.js"></script>
<style>
:root {
--bg: #0d1117; --card: #161b22; --border: #30363d;
--text: #c9d1d9; --muted: #8b949e; --accent: #58a6ff;
--green: #3fb950; --red: #f85149; --yellow: #d29922;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; background: var(--bg); color: var(--text); }
.container { max-width: 1440px; margin: 0 auto; padding: 20px; }
header { display: flex; justify-content: space-between; align-items: center; padding: 16px 0; border-bottom: 1px solid var(--border); margin-bottom: 20px; }
header h1 { font-size: 1.4rem; }
.badge { background: var(--border); padding: 4px 10px; border-radius: 12px; font-size: 0.75rem; }
.badge.green { background: #1a3a2a; color: var(--green); }
.badge.red { background: #3a1a1a; color: var(--red); }
.controls { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; align-items: center; }
button { background: var(--card); color: var(--text); border: 1px solid var(--border); padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 0.85rem; transition: all 0.15s; }
button:hover { border-color: var(--accent); color: var(--accent); }
button.primary { background: #1f3a5f; border-color: var(--accent); color: var(--accent); }
button.primary:hover { background: #2a4a6f; }
select { background: var(--card); color: var(--text); border: 1px solid var(--border); padding: 8px 12px; border-radius: 6px; font-size: 0.85rem; }
.ticker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; margin-bottom: 24px; }
.ticker-card { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 16px; transition: border-color 0.15s; }
.ticker-card:hover { border-color: var(--accent); }
.ticker-card h3 { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.ticker-card .price { font-size: 1.5rem; font-weight: 600; }
.ticker-card .meta { color: var(--muted); font-size: 0.8rem; margin-top: 4px; }
.prediction { margin-top: 12px; padding: 10px; border-radius: 6px; font-size: 0.85rem; }
.prediction.up { background: #1a3a2a; border-left: 3px solid var(--green); }
.prediction.down { background: #3a1a1a; border-left: 3px solid var(--red); }
.prediction.neutral { background: #2a2a1a; border-left: 3px solid var(--yellow); }
.prediction .dir { font-weight: 600; text-transform: uppercase; font-size: 0.9rem; }
.prediction .conf { color: var(--muted); font-size: 0.75rem; }
.chart-section { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 20px; margin-bottom: 20px; }
.chart-section h2 { font-size: 1.1rem; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center; }
.alerts-section { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 20px; margin-bottom: 20px; }
.alert-item { padding: 10px 14px; margin-bottom: 8px; border-radius: 6px; font-size: 0.85rem; display: flex; justify-content: space-between; align-items: center; }
.alert-item.high { background: #3a1a1a; border-left: 3px solid var(--red); }
.alert-item.medium { background: #2a2a1a; border-left: 3px solid var(--yellow); }
.alert-item.low { background: #1a2a3a; border-left: 3px solid var(--accent); }
.alert-item .time { color: var(--muted); font-size: 0.75rem; }
.stats-row { display: flex; gap: 20px; margin-bottom: 20px; flex-wrap: wrap; }
.stat-box { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 16px 24px; flex: 1; min-width: 150px; text-align: center; }
.stat-box .value { font-size: 1.6rem; font-weight: 700; }
.stat-box .label { color: var(--muted); font-size: 0.75rem; margin-top: 4px; text-transform: uppercase; }
.loading { text-align: center; padding: 40px; color: var(--muted); }
.spinner { display: inline-block; width: 24px; height: 24px; border: 3px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.progress { width: 100%; height: 4px; background: var(--border); border-radius: 2px; margin-top: 8px; }
.progress-bar { height: 100%; background: var(--accent); border-radius: 2px; transition: width 0.3s; }
.feature-importance { margin-top: 12px; }
.feature-bar { display: flex; align-items: center; margin-bottom: 4px; font-size: 0.75rem; }
.feature-bar .name { width: 140px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.feature-bar .bar { flex: 1; height: 6px; background: var(--border); border-radius: 3px; margin: 0 8px; }
.feature-bar .bar-fill { height: 100%; background: var(--accent); border-radius: 3px; }
.feature-bar .val { width: 40px; text-align: right; color: var(--muted); }
#mainChart { width: 100%; }
#indicatorChart { width: 100%; }
</style>
</head>
<body>
<div class="container">
<header>
<h1>📈 Stock Prediction Engine</h1>
<div style="display:flex;gap:10px;align-items:center;">
<span class="badge" id="statusBadge">Connecting...</span>
<span class="badge" id="lastUpdate">--</span>
<button onclick="refreshAll()" title="Refresh">↻</button>
</div>
</header>
<div class="stats-row" id="statsRow">
<div class="stat-box"><div class="value" id="statTickers">--</div><div class="label">Tickers</div></div>
<div class="stat-box"><div class="value" id="statModels">--</div><div class="label">Models Trained</div></div>
<div class="stat-box"><div class="value" id="statAlerts">--</div><div class="label">Active Alerts</div></div>
<div class="stat-box"><div class="value" id="statAvgAcc">--</div><div class="label">Avg Accuracy</div></div>
</div>
<div class="controls">
<select id="tickerSelect" onchange="onTickerChange()">
<option value="">Select Ticker...</option>
</select>
<button class="primary" onclick="trainSelected()">🧠 Train Model</button>
<button onclick="trainAll()">Train All</button>
<button onclick="fetchData()">↻ Fetch Data</button>
<button onclick="loadAlerts()">🔔 Alerts</button>
</div>
<div id="tickerGrid" class="ticker-grid"></div>
<div class="chart-section">
<h2>Price Chart <span id="chartTicker" style="color:var(--accent);font-size:0.9rem;"></span></h2>
<div id="mainChart"></div>
</div>
<div class="chart-section">
<h2>Technical Indicators</h2>
<div id="indicatorChart"></div>
</div>
<div class="chart-section" id="modelSection" style="display:none;">
<h2>Model Info <span id="modelTicker" style="color:var(--accent);font-size:0.9rem;"></span></h2>
<div id="modelMetrics"></div>
<div class="feature-importance" id="featureImportance"></div>
</div>
<div class="alerts-section" id="alertsSection" style="display:none;">
<h2 style="margin-bottom:12px;">🔔 Active Alerts</h2>
<div id="alertsList"></div>
</div>
</div>
<script>
const API = window.location.origin;
let tickerData = {};
let dashboardData = null;
async function api(path) {
const r = await fetch(API + path);
if (!r.ok) throw new Error(`${r.status}: ${path}`);
return r.json();
}
async function refreshAll() {
await loadDashboard();
await loadAlerts();
}
async function loadDashboard() {
try {
document.getElementById('statusBadge').textContent = 'Loading...';
document.getElementById('statusBadge').className = 'badge';
dashboardData = await api('/api/dashboard');
const tickers = dashboardData.tickers;
const select = document.getElementById('tickerSelect');
const grid = document.getElementById('tickerGrid');
// Update stats
const trained = tickers.filter(t => t.has_model).length;
const accs = tickers.filter(t => t.latest && t.prediction && t.prediction.model_accuracy)
.map(t => t.prediction.model_accuracy);
const avgAcc = accs.length ? (accs.reduce((a,b) => a+b, 0) / accs.length) : 0;
document.getElementById('statTickers').textContent = tickers.length;
document.getElementById('statModels').textContent = trained;
document.getElementById('statAvgAcc').textContent = avgAcc ? (avgAcc * 100).toFixed(1) + '%' : '--';
document.getElementById('lastUpdate').textContent = new Date(dashboardData.updated_at).toLocaleTimeString();
document.getElementById('statusBadge').textContent = 'Online';
document.getElementById('statusBadge').className = 'badge green';
// Populate select
select.innerHTML = '<option value="">Select Ticker...</option>';
tickers.forEach(t => {
const opt = document.createElement('option');
opt.value = t.ticker;
opt.textContent = t.ticker;
select.appendChild(opt);
});
// Build ticker cards
grid.innerHTML = '';
tickers.forEach(t => {
const card = document.createElement('div');
card.className = 'ticker-card';
card.onclick = () => { select.value = t.ticker; onTickerChange(); };
card.style.cursor = 'pointer';
let predHtml = '';
if (t.prediction) {
const p = t.prediction;
const dirClass = p.direction || 'neutral';
predHtml = `<div class="prediction ${dirClass}">
<div class="dir">${p.direction || 'neutral'} ${p.pct_change ? ' (' + (p.pct_change > 0 ? '+' : '') + p.pct_change.toFixed(2) + '%)' : ''}</div>
<div class="conf">Confidence: ${(p.confidence * 100).toFixed(1)}% | Accuracy: ${p.model_accuracy ? (p.model_accuracy * 100).toFixed(1) + '%' : '--'}</div>
</div>`;
} else if (t.has_model) {
predHtml = '<div class="prediction neutral"><div class="dir">Model Ready</div><div class="conf">Select to see prediction</div></div>';
} else {
predHtml = '<div class="prediction neutral"><div class="dir">Not Trained</div><div class="conf">Click "Train Model" to start</div></div>';
}
card.innerHTML = `
<h3>${t.ticker} ${t.has_model ? '<span class="badge green">✓</span>' : '<span class="badge">○</span>'}</h3>
<div class="price">$${t.latest ? t.latest.close?.toFixed(2) || '--' : '--'}</div>
<div class="meta">${t.latest ? t.latest.date?.split('T')[0] || '' : ''}</div>
${predHtml}
`;
grid.appendChild(card);
});
} catch (e) {
document.getElementById('statusBadge').textContent = 'Error';
document.getElementById('statusBadge').className = 'badge red';
console.error(e);
}
}
async function onTickerChange() {
const ticker = document.getElementById('tickerSelect').value;
if (!ticker) return;
await loadChart(ticker);
await loadModel(ticker);
}
async function loadChart(ticker) {
try {
document.getElementById('chartTicker').textContent = ticker;
const data = await api(`/api/data/${ticker}?limit=200`);
const rows = data.data;
const dates = rows.map(r => r.Date);
const closes = rows.map(r => r.Close);
const volumes = rows.map(r => r.Volume);
const sma20 = rows.map(r => r.SMA_20);
const ema12 = rows.map(r => r.EMA_12);
const bbUpper = rows.map(r => r.BB_upper);
const bbLower = rows.map(r => r.BB_lower);
// Price chart
const traces = [
{
x: dates, y: closes, type: 'candlestick',
open: rows.map(r => r.Open), high: rows.map(r => r.High),
low: rows.map(r => r.Low), close: closes,
name: 'Price', increasing: {line: {color: '#3fb950'}},
decreasing: {line: {color: '#f85149'}},
},
{ x: dates, y: sma20, type: 'scatter', mode: 'lines', name: 'SMA 20',
line: {color: '#d29922', width: 1.5}, yaxis: 'y' },
{ x: dates, y: ema12, type: 'scatter', mode: 'lines', name: 'EMA 12',
line: {color: '#58a6ff', width: 1.5}, yaxis: 'y' },
{ x: [...dates, ...dates.reverse()], y: [...bbUpper, ...bbLower.reverse()],
fill: 'toself', fillcolor: 'rgba(88,166,255,0.1)', line: {color: 'transparent'},
name: 'BB', yaxis: 'y', showlegend: true },
{ x: dates, y: volumes, type: 'bar', name: 'Volume', yaxis: 'y2',
marker: {color: volumes.map((v, i) => closes[i] >= (closes[i-1] || closes[i]) ? '#3fb950' : '#f85149')} },
];
Plotly.newPlot('mainChart', traces, {
paper_bgcolor: '#161b22', plot_bgcolor: '#161b22',
font: {color: '#8b949e', size: 11},
margin: {t: 30, r: 30, b: 40, l: 60},
xaxis: {gridcolor: '#30363d', rangeslider: {visible: false}},
yaxis: {gridcolor: '#30363d', domain: [0.35, 1]},
yaxis2: {gridcolor: '#30363d', domain: [0, 0.25]},
showlegend: true, legend: {bgcolor: 'transparent', font: {size: 10}},
}, {responsive: true, displayModeBar: false});
// Indicator chart
const rsi = rows.map(r => r.RSI);
const macdHist = rows.map(r => r.MACD_histogram);
const indTraces = [
{ x: dates, y: rsi, type: 'scatter', mode: 'lines', name: 'RSI',
line: {color: '#d29922', width: 1.5}, yaxis: 'y' },
{ x: dates, y: macdHist, type: 'bar', name: 'MACD Hist', yaxis: 'y2',
marker: {color: macdHist.map(v => v >= 0 ? '#3fb950' : '#f85149')} },
];
Plotly.newPlot('indicatorChart', indTraces, {
paper_bgcolor: '#161b22', plot_bgcolor: '#161b22',
font: {color: '#8b949e', size: 11},
margin: {t: 30, r: 30, b: 40, l: 60},
xaxis: {gridcolor: '#30363d'},
yaxis: {gridcolor: '#30363d', domain: [0.55, 1], range: [0, 100]},
yaxis2: {gridcolor: '#30363d', domain: [0, 0.45]},
showlegend: true, legend: {bgcolor: 'transparent', font: {size: 10}},
shapes: [
{type: 'line', y0: 70, y1: 70, x0: 0, x1: 1, xref: 'paper', line: {color: '#f85149', width: 1, dash: 'dot'}, yaxis: 'y'},
{type: 'line', y0: 30, y1: 30, x0: 0, x1: 1, xref: 'paper', line: {color: '#3fb950', width: 1, dash: 'dot'}, yaxis: 'y'},
],
}, {responsive: true, displayModeBar: false});
} catch (e) {
console.error('Chart error:', e);
}
}
async function loadModel(ticker) {
try {
const info = await api(`/api/model/${ticker}`);
document.getElementById('modelSection').style.display = 'block';
document.getElementById('modelTicker').textContent = ticker;
const m = info.metrics;
document.getElementById('modelMetrics').innerHTML = `
<div class="stats-row">
<div class="stat-box"><div class="value">${(m.classification.accuracy * 100).toFixed(1)}%</div><div class="label">Accuracy</div></div>
<div class="stat-box"><div class="value">${(m.classification.precision * 100).toFixed(1)}%</div><div class="label">Precision</div></div>
<div class="stat-box"><div class="value">${(m.classification.recall * 100).toFixed(1)}%</div><div class="label">Recall</div></div>
<div class="stat-box"><div class="value">${(m.classification.f1 * 100).toFixed(1)}%</div><div class="label">F1 Score</div></div>
<div class="stat-box"><div class="value">${m.regression.r2.toFixed(3)}</div><div class="label">R²</div></div>
<div class="stat-box"><div class="value">${m.train_size}</div><div class="label">Training Bars</div></div>
</div>
<div style="color:var(--muted);font-size:0.75rem;">Trained: ${info.trained_at} | Features: ${info.feature_count} | MAE: ${m.regression.mae.toFixed(4)}% | RMSE: ${m.regression.rmse.toFixed(4)}%</div>
`;
// Feature importance
const impDiv = document.getElementById('featureImportance');
const maxImp = info.feature_importance[0]?.importance || 1;
impDiv.innerHTML = '<h3 style="margin:16px 0 8px;font-size:0.9rem;">Feature Importance</h3>' +
info.feature_importance.map(f => `
<div class="feature-bar">
<span class="name">${f.feature}</span>
<div class="bar"><div class="bar-fill" style="width:${(f.importance / maxImp * 100).toFixed(0)}%"></div></div>
<span class="val">${f.importance.toFixed(3)}</span>
</div>
`).join('');
} catch (e) {
document.getElementById('modelSection').style.display = 'none';
}
}
async function loadAlerts() {
try {
const data = await api('/api/alerts');
const section = document.getElementById('alertsSection');
const list = document.getElementById('alertsList');
document.getElementById('statAlerts').textContent = data.count;
if (data.count === 0) {
section.style.display = 'none';
return;
}
section.style.display = 'block';
list.innerHTML = data.alerts.map(a => `
<div class="alert-item ${a.severity}">
<div>
<strong>${a.ticker}</strong> — ${a.message}
${a.type === 'prediction' ? `<span class="badge ${a.direction === 'up' ? 'green' : 'red'}">${a.direction} ${(a.confidence * 100).toFixed(0)}%</span>` : ''}
</div>
<div class="time">${a.timestamp?.split('T')[1] || ''}</div>
</div>
`).join('');
} catch (e) {
console.error('Alert error:', e);
}
}
async function trainSelected() {
const ticker = document.getElementById('tickerSelect').value;
if (!ticker) return alert('Select a ticker first');
await trainTicker(ticker);
}
async function trainTicker(ticker) {
try {
const btn = event.target;
btn.textContent = 'Training...';
btn.disabled = true;
const result = await api(`/api/train/${ticker}`);
btn.textContent = `✓ ${result.metrics.classification.accuracy.toFixed(1)}`;
setTimeout(() => { btn.textContent = '🧠 Train Model'; btn.disabled = false; }, 2000);
await loadDashboard();
if (document.getElementById('tickerSelect').value === ticker) {
await loadChart(ticker);
await loadModel(ticker);
}
} catch (e) {
event.target.textContent = '✗ Error';
setTimeout(() => { event.target.textContent = '🧠 Train Model'; }, 2000);
alert('Training failed: ' + e.message);
}
}
async function trainAll() {
try {
event.target.textContent = 'Training all...';
event.target.disabled = true;
const result = await api('/api/train_all');
const ok = result.results.filter(r => r.status === 'ok').length;
event.target.textContent = `✓ ${ok}/${result.results.length}`;
setTimeout(() => { event.target.textContent = 'Train All'; event.target.disabled = false; }, 3000);
await loadDashboard();
} catch (e) {
alert('Train all failed: ' + e.message);
event.target.textContent = 'Train All';
event.target.disabled = false;
}
}
async function fetchData() {
try {
event.target.textContent = 'Fetching...';
event.target.disabled = true;
const data = await api('/api/tickers');
event.target.textContent = `✓ ${data.tickers.length}`;
setTimeout(() => { event.target.textContent = '↻ Fetch Data'; event.target.disabled = false; }, 2000);
} catch (e) {
event.target.textContent = '↻ Fetch Data';
event.target.disabled = false;
}
}
// Init
refreshAll();
// Auto-refresh every 5 minutes
setInterval(refreshAll, 300000);
</script>
</body>
</html>