add whale/public picks to prediction history and new API endpoint

- Add _compute_whale_public_picks() to reconstruct whale/public picks from historical bets
- Merge whale_pick, public_pick, whale_hit, public_hit into last_20_predictions
- Add get_prediction_history(limit) for lightweight prediction+accuracy data
- Add /api/prediction-history endpoint (default 100, max 500)
- Add Whale and Public columns with HIT/MISS to Last 20 table in frontend
This commit is contained in:
2026-02-26 09:42:16 +05:00
parent 54501260b4
commit d1dc8f62fa
3 changed files with 225 additions and 3 deletions

View File

@@ -262,7 +262,7 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
<div class="section-title">Last 20 Predictions vs Actual</div>
<div class="panel" style="max-height:500px;overflow-y:auto">
<table class="pred-history" id="pred-history">
<thead><tr><th>Game</th><th>Predicted</th><th>2nd Pick</th><th>P(A)</th><th>P(B)</th><th>P(C)</th><th>Actual</th><th>Result</th></tr></thead>
<thead><tr><th>Game</th><th>Predicted</th><th>2nd Pick</th><th>P(A)</th><th>P(B)</th><th>P(C)</th><th>Whale</th><th>Public</th><th>Actual</th><th>Result</th></tr></thead>
<tbody></tbody>
</table>
</div>
@@ -666,16 +666,46 @@ function renderCrowdStats(data) {
function renderLast20(predictions) {
const tbody = $('pred-history').querySelector('tbody');
if (!predictions || predictions.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" style="color:var(--text3)">Not enough data</td></tr>';
tbody.innerHTML = '<tr><td colspan="10" style="color:var(--text3)">Not enough data</td></tr>';
return;
}
const fullHits = predictions.filter(p => p.correct).length;
const semiHits = predictions.filter(p => p.semi_correct).length;
const score = fullHits + semiHits * 0.5;
// Whale/public accuracy
const whaleEntries = predictions.filter(p => p.whale_pick != null);
const whaleHits = whaleEntries.filter(p => p.whale_hit).length;
const publicEntries = predictions.filter(p => p.public_pick != null);
const publicHits = publicEntries.filter(p => p.public_hit).length;
const whalePct = whaleEntries.length > 0 ? (whaleHits / whaleEntries.length * 100).toFixed(1) : '--';
const publicPct = publicEntries.length > 0 ? (publicHits / publicEntries.length * 100).toFixed(1) : '--';
tbody.innerHTML = predictions.slice().reverse().map(p => {
const maxProb = Math.max(p.probs.A, p.probs.B, p.probs.C);
const resultClass = p.correct ? 'correct' : (p.semi_correct ? 'semi' : 'wrong');
const resultLabel = p.correct ? 'HIT' : (p.semi_correct ? 'SEMI' : 'MISS');
// Whale cell
let whaleCell;
if (p.whale_pick) {
const whCls = p.whale_hit ? 'correct' : 'wrong';
const whLabel = p.whale_hit ? 'HIT' : 'MISS';
whaleCell = `<td><span style="color:${CHAIR_COLORS[p.whale_pick]};font-weight:700">${p.whale_pick}</span> <span class="${whCls}" style="font-size:10px">${whLabel}</span></td>`;
} else {
whaleCell = '<td style="color:var(--text3)">--</td>';
}
// Public cell
let pubCell;
if (p.public_pick) {
const puCls = p.public_hit ? 'correct' : 'wrong';
const puLabel = p.public_hit ? 'HIT' : 'MISS';
pubCell = `<td><span style="color:${CHAIR_COLORS[p.public_pick]};font-weight:700">${p.public_pick}</span> <span class="${puCls}" style="font-size:10px">${puLabel}</span></td>`;
} else {
pubCell = '<td style="color:var(--text3)">--</td>';
}
return `<tr>
<td style="font-weight:700">#${p.game_no}</td>
<td class="winner-cell" style="color:${CHAIR_COLORS[p.predicted]}">${p.predicted}</td>
@@ -684,14 +714,18 @@ function renderLast20(predictions) {
const w = p.probs[c] / maxProb * 40;
return `<td><span class="prob-bar" style="width:${w}px;background:${CHAIR_COLORS[c]}"></span> ${pct(p.probs[c])}</td>`;
}).join('')}
${whaleCell}
${pubCell}
<td class="winner-cell" style="color:${CHAIR_COLORS[p.actual]}">${p.actual}</td>
<td class="${resultClass}" style="font-weight:700">${resultLabel}</td>
</tr>`;
}).join('') +
`<tr style="border-top:2px solid var(--border);font-weight:700">
<td colspan="6" style="text-align:right;color:var(--text2)">Accuracy (last ${predictions.length})</td>
<td style="color:${whalePct !== '--' && parseFloat(whalePct) > 33.3 ? 'var(--green)' : 'var(--text2)'}">${whalePct !== '--' ? whalePct + '%' : '--'}</td>
<td style="color:${publicPct !== '--' && parseFloat(publicPct) > 33.3 ? 'var(--green)' : 'var(--text2)'}">${publicPct !== '--' ? publicPct + '%' : '--'}</td>
<td colspan="2" style="color:${score/predictions.length > 1/3 ? 'var(--green)' : 'var(--red)'}">
${(score/predictions.length*100).toFixed(1)}% (${score}/${predictions.length})
Model: ${(score/predictions.length*100).toFixed(1)}%
</td>
</tr>`;
}