add predictions page with game theory analysis and card stats

Bayesian next-chair predictor (Markov chains, base rate, streak regression),
statistical tests (chi-squared, runs test, autocorrelation), theory
backtesting with rolling accuracy, and card-level analysis (value/suit
distribution, face card frequency, top winning cards).
This commit is contained in:
2026-02-25 23:16:37 +05:00
parent d8ec792a88
commit b07b073cc0
6 changed files with 1003 additions and 0 deletions

View File

@@ -215,6 +215,7 @@
<div style="display:flex;gap:14px">
<a href="/" class="nav-link">Live Dashboard &rarr;</a>
<a href="/patterns" class="nav-link">Patterns &rarr;</a>
<a href="/predictions" class="nav-link">Predictions &rarr;</a>
</div>
</div>

View File

@@ -603,6 +603,7 @@
<div class="status">
<a href="/analytics" style="color:var(--accent);text-decoration:none;font-size:12px;font-weight:600;margin-right:10px">Analytics &rarr;</a>
<a href="/patterns" style="color:var(--accent);text-decoration:none;font-size:12px;font-weight:600;margin-right:10px">Patterns &rarr;</a>
<a href="/predictions" style="color:var(--accent);text-decoration:none;font-size:12px;font-weight:600;margin-right:10px">Predictions &rarr;</a>
<div id="status-dot" class="status-dot"></div>
<span id="status-text">Connecting...</span>
</div>

View File

@@ -123,6 +123,7 @@
<div class="nav-links">
<a href="/" class="nav-link">Live Dashboard &rarr;</a>
<a href="/analytics" class="nav-link">Analytics &rarr;</a>
<a href="/predictions" class="nav-link">Predictions &rarr;</a>
</div>
</div>

528
static/predictions.html Normal file
View File

@@ -0,0 +1,528 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Predictions &amp; Game Theory</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<style>
:root {
--bg: #0f1117; --surface: #1a1d27; --surface2: #232736; --surface3: #2a2e42;
--border: #2d3148; --text: #e4e6f0; --text2: #8b8fa3; --text3: #5a5f75;
--accent: #6c5ce7;
--chair-a: #3b82f6; --chair-b: #ec4899; --chair-c: #f59e0b;
--green: #10b981; --red: #ef4444;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--text); }
.header {
display: flex; align-items: center; justify-content: space-between;
padding: 10px 20px; background: var(--surface); border-bottom: 1px solid var(--border);
}
.header h1 { font-size: 15px; font-weight: 700; letter-spacing: -0.3px; }
.nav-links { display: flex; gap: 14px; }
.nav-link {
font-size: 12px; color: var(--accent); text-decoration: none;
font-weight: 600; transition: color 0.2s;
}
.nav-link:hover { color: #a78bfa; }
.content { padding: 16px 20px; max-width: 1400px; margin: 0 auto; }
.loading { text-align: center; padding: 60px; color: var(--text2); font-size: 14px; }
.section { margin-bottom: 24px; }
.section-title {
font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px;
color: var(--text2); margin-bottom: 12px; padding-bottom: 6px; border-bottom: 1px solid var(--border);
}
/* Hero prediction cards */
.pred-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 16px; }
.pred-card {
background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
padding: 24px; text-align: center; position: relative; transition: all 0.3s;
}
.pred-card.recommended {
border-color: var(--accent);
box-shadow: 0 0 20px rgba(108, 92, 231, 0.3), 0 0 40px rgba(108, 92, 231, 0.1);
}
.pred-card .chair-label { font-size: 14px; font-weight: 700; margin-bottom: 8px; }
.pred-card .prob { font-size: 42px; font-weight: 800; letter-spacing: -2px; }
.pred-card .badge {
display: inline-block; margin-top: 8px; padding: 3px 10px; border-radius: 12px;
font-size: 11px; font-weight: 600; background: var(--accent); color: #fff;
}
.signal-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.signal-table th, .signal-table td {
padding: 8px 12px; text-align: center; border-bottom: 1px solid var(--border);
}
.signal-table th { color: var(--text2); font-weight: 600; text-transform: uppercase; font-size: 11px; }
/* Two-column layout */
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.panel {
background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 16px;
}
.panel-title { font-size: 12px; font-weight: 700; color: var(--text2); margin-bottom: 12px; text-transform: uppercase; }
/* Heatmap tables */
.heatmap { width: 100%; border-collapse: collapse; font-size: 12px; }
.heatmap th, .heatmap td { padding: 6px 8px; text-align: center; border: 1px solid var(--border); }
.heatmap th { background: var(--surface2); color: var(--text2); font-weight: 600; font-size: 11px; }
/* Stat cards */
.stat-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.stat-card {
background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
padding: 20px; text-align: center;
}
.stat-card .label { font-size: 12px; color: var(--text2); font-weight: 600; margin-bottom: 4px; }
.stat-card .value { font-size: 28px; font-weight: 800; }
/* Test result panels */
.test-result { margin-bottom: 8px; }
.test-result .test-label { font-size: 11px; color: var(--text3); font-weight: 600; text-transform: uppercase; }
.test-result .test-value { font-size: 16px; font-weight: 700; }
.test-result .test-interpret { font-size: 12px; color: var(--text2); margin-top: 2px; }
.significant { color: var(--red); }
.not-significant { color: var(--green); }
/* Chart containers */
.chart-container { position: relative; height: 300px; }
.chart-container-sm { position: relative; height: 250px; }
/* Backtest */
.backtest-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin-bottom: 16px; }
.bt-card {
background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
padding: 14px; text-align: center;
}
.bt-card .bt-name { font-size: 11px; color: var(--text2); font-weight: 600; text-transform: uppercase; margin-bottom: 4px; }
.bt-card .bt-acc { font-size: 24px; font-weight: 800; }
.bt-card .bt-acc.above { color: var(--green); }
.bt-card .bt-acc.below { color: var(--red); }
@media (max-width: 768px) {
.pred-cards, .two-col, .stat-cards { grid-template-columns: 1fr; }
.backtest-grid { grid-template-columns: repeat(2, 1fr); }
.pred-card .prob { font-size: 32px; }
}
</style>
</head>
<body>
<div class="header">
<h1>Predictions &amp; Game Theory</h1>
<div class="nav-links">
<a href="/" class="nav-link">Live Dashboard &rarr;</a>
<a href="/analytics" class="nav-link">Analytics &rarr;</a>
<a href="/patterns" class="nav-link">Patterns &rarr;</a>
</div>
</div>
<div class="content">
<div id="loading" class="loading">Loading prediction data...</div>
<div id="main" style="display:none">
<!-- Bayesian Prediction Hero -->
<div class="section">
<div class="section-title">Bayesian Next-Chair Prediction</div>
<div id="pred-cards" class="pred-cards"></div>
<div class="panel" style="margin-top:8px">
<div class="panel-title">Signal Breakdown</div>
<table class="signal-table" id="signal-table">
<thead><tr><th>Signal</th><th>Weight</th><th>A</th><th>B</th><th>C</th></tr></thead>
<tbody></tbody>
</table>
</div>
</div>
<!-- Markov Matrices -->
<div class="section">
<div class="section-title">Markov Transition Matrices</div>
<div class="two-col">
<div class="panel">
<div class="panel-title">1st Order &mdash; P(next | last)</div>
<table class="heatmap" id="markov1-table"></table>
</div>
<div class="panel">
<div class="panel-title">2nd Order &mdash; P(next | last two)</div>
<table class="heatmap" id="markov2-table"></table>
</div>
</div>
</div>
<!-- Autocorrelation -->
<div class="section">
<div class="section-title">Autocorrelation (Lags 1&ndash;5)</div>
<div class="panel">
<div class="chart-container-sm"><canvas id="autocorr-chart"></canvas></div>
</div>
</div>
<!-- Statistical Tests -->
<div class="section">
<div class="section-title">Statistical Tests</div>
<div class="two-col">
<div class="panel">
<div class="panel-title">Chi-Squared Goodness of Fit</div>
<div id="chi-results"></div>
</div>
<div class="panel">
<div class="panel-title">Wald-Wolfowitz Runs Test</div>
<div id="runs-results"></div>
</div>
</div>
</div>
<!-- Theory Backtesting -->
<div class="section">
<div class="section-title">Theory Backtesting</div>
<div id="backtest-cards" class="backtest-grid"></div>
<div class="panel">
<div class="panel-title">Rolling Accuracy (Last 200 Games)</div>
<div class="chart-container"><canvas id="backtest-chart"></canvas></div>
</div>
</div>
<!-- Card Value Distribution -->
<div class="section">
<div class="section-title">Card Value Distribution by Chair (Last 500 Games)</div>
<div class="panel">
<div class="chart-container"><canvas id="card-value-chart"></canvas></div>
</div>
</div>
<!-- Face Card Frequency -->
<div class="section">
<div class="section-title">Face Card Frequency (J, Q, K, A)</div>
<div id="face-cards" class="stat-cards"></div>
</div>
<!-- Suit Distribution -->
<div class="section">
<div class="section-title">Suit Distribution by Chair</div>
<div class="panel">
<div class="chart-container-sm"><canvas id="suit-chart"></canvas></div>
</div>
</div>
<!-- Top Winning Cards -->
<div class="section">
<div class="section-title">Top 20 Cards in Winning Hands</div>
<div class="panel">
<div class="chart-container"><canvas id="winning-cards-chart"></canvas></div>
</div>
</div>
</div>
</div>
<script>
const $ = id => document.getElementById(id);
const CHAIRS = ['A', 'B', 'C'];
const CHAIR_COLORS = {A:'#3b82f6', B:'#ec4899', C:'#f59e0b'};
const CHART_DEFAULTS = {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { labels: { color: '#8b8fa3', font: { size: 11 } } } },
scales: {
x: { ticks: { color: '#5a5f75', font: { size: 10 } }, grid: { color: '#2d3148' } },
y: { ticks: { color: '#5a5f75', font: { size: 10 } }, grid: { color: '#2d3148' } }
}
};
function heatColor(val) {
// val is probability 0-1, center at 0.333
const diff = val - 1/3;
if (diff > 0) {
const intensity = Math.min(diff * 6, 1);
return `rgba(16, 185, 129, ${0.15 + intensity * 0.5})`; // green
} else {
const intensity = Math.min(Math.abs(diff) * 6, 1);
return `rgba(239, 68, 68, ${0.15 + intensity * 0.5})`; // red
}
}
function pct(v) { return (v * 100).toFixed(1) + '%'; }
function renderPrediction(data) {
const container = $('pred-cards');
const best = CHAIRS.reduce((a, b) => data.prediction[a] > data.prediction[b] ? a : b);
container.innerHTML = CHAIRS.map(c => {
const p = data.prediction[c];
const isBest = c === best;
return `<div class="pred-card ${isBest ? 'recommended' : ''}">
<div class="chair-label" style="color:${CHAIR_COLORS[c]}">Chair ${c}</div>
<div class="prob" style="color:${CHAIR_COLORS[c]}">${pct(p)}</div>
${isBest ? '<div class="badge">Recommended</div>' : ''}
</div>`;
}).join('');
// Signal table
const tbody = $('signal-table').querySelector('tbody');
const sigNames = {'base_rate':'Base Rate','markov_1':'Markov-1','markov_2':'Markov-2','recent_20':'Recent 20','streak':'Streak'};
tbody.innerHTML = Object.entries(data.signals).map(([key, sig]) =>
`<tr><td style="text-align:left">${sigNames[key]||key}</td><td>${(sig.weight*100).toFixed(0)}%</td>` +
CHAIRS.map(c => `<td style="color:${CHAIR_COLORS[c]}">${pct(sig.probs[c])}</td>`).join('') + '</tr>'
).join('') +
`<tr style="font-weight:700;border-top:2px solid var(--border)"><td style="text-align:left">Combined</td><td>100%</td>` +
CHAIRS.map(c => `<td style="color:${CHAIR_COLORS[c]}">${pct(data.prediction[c])}</td>`).join('') + '</tr>';
}
function renderMarkov1(m) {
const t = $('markov1-table');
let html = '<tr><th>From \\ To</th>' + CHAIRS.map(c => `<th style="color:${CHAIR_COLORS[c]}">→${c}</th>`).join('') + '</tr>';
for (const src of CHAIRS) {
html += `<tr><th style="color:${CHAIR_COLORS[src]}">${src}</th>`;
for (const dst of CHAIRS) {
const v = m.matrix[src]?.[dst] || 0;
html += `<td style="background:${heatColor(v)}">${pct(v)}</td>`;
}
html += '</tr>';
}
t.innerHTML = html;
}
function renderMarkov2(m) {
const t = $('markov2-table');
const keys = [];
for (const a of CHAIRS) for (const b of CHAIRS) keys.push(a+b);
let html = '<tr><th>From \\ To</th>' + CHAIRS.map(c => `<th style="color:${CHAIR_COLORS[c]}">→${c}</th>`).join('') + '</tr>';
for (const key of keys) {
html += `<tr><th>${key}</th>`;
for (const dst of CHAIRS) {
const v = m.matrix[key]?.[dst] || 0;
html += `<td style="background:${heatColor(v)}">${pct(v)}</td>`;
}
html += '</tr>';
}
t.innerHTML = html;
}
function renderAutocorrelation(ac) {
const ctx = $('autocorr-chart').getContext('2d');
const threshold = ac.length > 0 && ac[0].significant !== undefined ? 1.96 / Math.sqrt(100) : 0.196;
new Chart(ctx, {
type: 'bar',
data: {
labels: ac.map(a => `Lag ${a.lag}`),
datasets: [{
label: 'Autocorrelation',
data: ac.map(a => a.r),
backgroundColor: ac.map(a => a.significant ? '#ef4444' : '#6c5ce7'),
borderRadius: 4,
}]
},
options: {
...CHART_DEFAULTS,
plugins: {
...CHART_DEFAULTS.plugins,
annotation: undefined,
legend: { display: false },
},
scales: {
...CHART_DEFAULTS.scales,
y: { ...CHART_DEFAULTS.scales.y, suggestedMin: -0.3, suggestedMax: 0.3 }
}
},
plugins: [{
id: 'thresholdLines',
afterDraw(chart) {
const {ctx, chartArea, scales} = chart;
const yScale = scales.y;
ctx.save();
ctx.setLineDash([5, 5]);
ctx.strokeStyle = '#ef444480';
ctx.lineWidth = 1;
for (const val of [threshold, -threshold]) {
const y = yScale.getPixelForValue(val);
ctx.beginPath();
ctx.moveTo(chartArea.left, y);
ctx.lineTo(chartArea.right, y);
ctx.stroke();
}
ctx.restore();
}
}]
});
}
function renderChiSquared(chi) {
$('chi-results').innerHTML = `
<div class="test-result"><div class="test-label">Chi-Squared Statistic</div>
<div class="test-value">${chi.chi2}</div></div>
<div class="test-result"><div class="test-label">p-value</div>
<div class="test-value ${chi.significant ? 'significant' : 'not-significant'}">${chi.p_value}</div></div>
<div class="test-result"><div class="test-label">Expected Count (each chair)</div>
<div class="test-value">${chi.expected}</div></div>
<div class="test-result"><div class="test-label">Observed</div>
<div class="test-value">A: ${chi.counts.A} &nbsp; B: ${chi.counts.B} &nbsp; C: ${chi.counts.C}</div></div>
<div class="test-result"><div class="test-label">Interpretation</div>
<div class="test-interpret ${chi.significant ? 'significant' : 'not-significant'}">
${chi.significant ? 'Distribution is significantly non-uniform (p < 0.05)' : 'Distribution is consistent with uniform (p >= 0.05)'}
</div></div>`;
}
function renderRunsTest(runs) {
$('runs-results').innerHTML = `
<div class="test-result"><div class="test-label">Observed Runs</div>
<div class="test-value">${runs.runs}</div></div>
<div class="test-result"><div class="test-label">Expected Runs</div>
<div class="test-value">${runs.expected_runs || '—'}</div></div>
<div class="test-result"><div class="test-label">Z-Score</div>
<div class="test-value">${runs.z_score}</div></div>
<div class="test-result"><div class="test-label">p-value</div>
<div class="test-value ${runs.significant ? 'significant' : 'not-significant'}">${runs.p_value}</div></div>
<div class="test-result"><div class="test-label">Interpretation</div>
<div class="test-interpret ${runs.significant ? 'significant' : 'not-significant'}">${runs.interpretation}</div></div>`;
}
function renderBacktest(bt) {
if (bt.error) {
$('backtest-cards').innerHTML = `<div style="color:var(--text2)">${bt.error}</div>`;
return;
}
const names = {base_rate:'Base Rate',markov_1:'Markov-1',markov_2:'Markov-2',recent_20:'Recent 20',streak:'Streak',combined:'Combined'};
$('backtest-cards').innerHTML = Object.entries(bt.accuracy).map(([key, acc]) =>
`<div class="bt-card"><div class="bt-name">${names[key]||key}</div>
<div class="bt-acc ${acc > bt.random_baseline ? 'above' : 'below'}">${acc}%</div>
<div style="font-size:10px;color:var(--text3);margin-top:4px">vs ${bt.random_baseline}% random</div>
</div>`
).join('');
if (bt.rolling_accuracy) {
const ctx = $('backtest-chart').getContext('2d');
const colors = {base_rate:'#8b8fa3',markov_1:'#3b82f6',markov_2:'#ec4899',recent_20:'#f59e0b',streak:'#10b981',combined:'#6c5ce7'};
const datasets = Object.entries(bt.rolling_accuracy).map(([key, data]) => ({
label: names[key]||key, data,
borderColor: colors[key]||'#fff', backgroundColor: 'transparent',
borderWidth: key === 'combined' ? 3 : 1.5,
pointRadius: 0, tension: 0.3,
}));
new Chart(ctx, {
type: 'line',
data: { labels: bt.rolling_accuracy.combined.map((_, i) => i + 1), datasets },
options: {
...CHART_DEFAULTS,
scales: {
...CHART_DEFAULTS.scales,
y: { ...CHART_DEFAULTS.scales.y, title: { display: true, text: 'Accuracy %', color: '#5a5f75' } },
x: { ...CHART_DEFAULTS.scales.x, title: { display: true, text: 'Game', color: '#5a5f75' },
ticks: { ...CHART_DEFAULTS.scales.x.ticks, maxTicksLimit: 10 } }
}
},
plugins: [{
id: 'baselineLine',
afterDraw(chart) {
const {ctx, chartArea, scales} = chart;
const y = scales.y.getPixelForValue(bt.random_baseline);
ctx.save();
ctx.setLineDash([5, 5]);
ctx.strokeStyle = '#ef444480';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(chartArea.left, y);
ctx.lineTo(chartArea.right, y);
ctx.stroke();
ctx.restore();
}
}]
});
}
}
function renderCardValues(cv) {
const ctx = $('card-value-chart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: cv.labels,
datasets: CHAIRS.map(c => ({
label: `Chair ${c}`, data: cv.labels.map(v => cv.chairs[c]?.[v] || 0),
backgroundColor: CHAIR_COLORS[c] + '99', borderColor: CHAIR_COLORS[c],
borderWidth: 1, borderRadius: 3,
}))
},
options: CHART_DEFAULTS
});
}
function renderFaceCards(fc) {
$('face-cards').innerHTML = CHAIRS.map(c => {
const d = fc[c];
return `<div class="stat-card">
<div class="label" style="color:${CHAIR_COLORS[c]}">Chair ${c}</div>
<div class="value" style="color:${CHAIR_COLORS[c]}">${d.pct}%</div>
<div style="font-size:11px;color:var(--text3);margin-top:4px">${d.face_cards} / ${d.total_cards} cards</div>
</div>`;
}).join('');
}
function renderSuits(s) {
const ctx = $('suit-chart').getContext('2d');
const suitColors = {'\u2660':'#e4e6f0','\u2665':'#ef4444','\u2663':'#10b981','\u2666':'#3b82f6'};
new Chart(ctx, {
type: 'bar',
data: {
labels: CHAIRS.map(c => `Chair ${c}`),
datasets: s.labels.map(suit => ({
label: suit, data: CHAIRS.map(c => s.chairs[c]?.[suit] || 0),
backgroundColor: (suitColors[suit] || '#8b8fa3') + '99',
borderColor: suitColors[suit] || '#8b8fa3',
borderWidth: 1, borderRadius: 3,
}))
},
options: CHART_DEFAULTS
});
}
function renderWinningCards(wc) {
const ctx = $('winning-cards-chart').getContext('2d');
// Color hearts/diamonds red, spades/clubs white
const colors = wc.map(c => {
const suit = c.card.slice(-1);
return (suit === '\u2665' || suit === '\u2666') ? '#ef4444' : '#e4e6f0';
});
new Chart(ctx, {
type: 'bar',
data: {
labels: wc.map(c => c.card),
datasets: [{
label: 'Appearances in Winning Hands',
data: wc.map(c => c.count),
backgroundColor: colors.map(c => c + '80'),
borderColor: colors,
borderWidth: 1, borderRadius: 3,
}]
},
options: {
...CHART_DEFAULTS,
indexAxis: 'y',
plugins: { legend: { display: false } },
}
});
}
function render(data) {
renderPrediction(data);
renderMarkov1(data.markov1);
renderMarkov2(data.markov2);
renderAutocorrelation(data.autocorrelation);
renderChiSquared(data.chi_squared);
renderRunsTest(data.runs_test);
renderBacktest(data.backtest);
renderCardValues(data.card_values);
renderFaceCards(data.face_cards);
renderSuits(data.suits);
renderWinningCards(data.winning_cards);
$('loading').style.display = 'none';
$('main').style.display = 'block';
}
fetch('/api/predictions')
.then(r => r.json())
.then(render)
.catch(err => { $('loading').textContent = 'Failed to load: ' + err.message; });
</script>
</body>
</html>