add whale/public semi-win scoring, expand to last 50, and full history modal

- Whale/public picks now track 2nd pick and score SEMI (0.5 pts) like model
- Prediction table expanded from 20 to 50 rows
- "View All History" modal with pagination (50/page), fetches up to 500
- Accuracy rows use semi-win scoring for all three columns
This commit is contained in:
2026-02-26 09:51:08 +05:00
parent d1dc8f62fa
commit 5fd4894599
2 changed files with 183 additions and 43 deletions

View File

@@ -1260,7 +1260,8 @@ def _compute_whale_public_picks(client, game_nos):
for gno in game_nos:
users = game_data.get(gno, {})
if not users:
picks[gno] = {"whale_pick": None, "public_pick": None,
picks[gno] = {"whale_pick": None, "whale_second_pick": None,
"public_pick": None, "public_second_pick": None,
"whale_count": 0, "public_count": 0,
"bettor_counts": {"A": 0, "B": 0, "C": 0}}
continue
@@ -1280,7 +1281,13 @@ def _compute_whale_public_picks(client, game_nos):
for c in CHAIR_LABELS:
whale_money[c] += users[uid].get(c, 0)
whale_total = sum(whale_money.values())
whale_pick = max(CHAIR_LABELS, key=lambda c: whale_money[c]) if whale_total > 0 else None
if whale_total > 0:
whale_ranked = sorted(CHAIR_LABELS, key=lambda c: whale_money[c], reverse=True)
whale_pick = whale_ranked[0]
whale_second_pick = whale_ranked[1]
else:
whale_pick = None
whale_second_pick = None
# Sum public money per chair
pub_money = {"A": 0, "B": 0, "C": 0}
@@ -1289,7 +1296,13 @@ def _compute_whale_public_picks(client, game_nos):
pub_money[c] += users[uid].get(c, 0)
pub_total = sum(pub_money.values())
total_bettors = len(user_totals)
public_pick = max(CHAIR_LABELS, key=lambda c: pub_money[c]) if pub_total > 0 and total_bettors > 5 else None
if pub_total > 0 and total_bettors > 5:
pub_ranked = sorted(CHAIR_LABELS, key=lambda c: pub_money[c], reverse=True)
public_pick = pub_ranked[0]
public_second_pick = pub_ranked[1]
else:
public_pick = None
public_second_pick = None
# Count bettors per chair
bettor_counts = {"A": 0, "B": 0, "C": 0}
@@ -1300,7 +1313,9 @@ def _compute_whale_public_picks(client, game_nos):
picks[gno] = {
"whale_pick": whale_pick,
"whale_second_pick": whale_second_pick,
"public_pick": public_pick,
"public_second_pick": public_second_pick,
"whale_count": len(whale_uids),
"public_count": len(pub_uids),
"bettor_counts": bettor_counts,
@@ -1372,25 +1387,29 @@ def get_prediction_analysis() -> dict:
# Backtesting
backtest = _backtest_theories(winners)
# Last 20 prediction vs actual
last_20_raw = _last_n_predictions(winners, 20)
# Attach game_nos to last_20
for entry in last_20_raw:
# Last 50 prediction vs actual
last_50_raw = _last_n_predictions(winners, 50)
# Attach game_nos to last_50
for entry in last_50_raw:
idx = entry["index"]
entry["game_no"] = game_nos[idx] if idx < len(game_nos) else 0
# Merge whale/public picks into last_20
last_20_game_nos = [e["game_no"] for e in last_20_raw if e.get("game_no")]
wp_data = _compute_whale_public_picks(client, last_20_game_nos)
for entry in last_20_raw:
# Merge whale/public picks into last_50
last_50_game_nos = [e["game_no"] for e in last_50_raw if e.get("game_no")]
wp_data = _compute_whale_public_picks(client, last_50_game_nos)
for entry in last_50_raw:
gno = entry.get("game_no", 0)
wp = wp_data.get(gno, {})
entry["whale_pick"] = wp.get("whale_pick")
entry["whale_second_pick"] = wp.get("whale_second_pick")
entry["public_pick"] = wp.get("public_pick")
entry["public_second_pick"] = wp.get("public_second_pick")
entry["bettor_counts"] = wp.get("bettor_counts", {"A": 0, "B": 0, "C": 0})
actual = entry["actual"]
entry["whale_hit"] = (wp.get("whale_pick") == actual) if wp.get("whale_pick") else None
entry["whale_semi"] = (not entry["whale_hit"] and wp.get("whale_second_pick") == actual) if wp.get("whale_pick") else None
entry["public_hit"] = (wp.get("public_pick") == actual) if wp.get("public_pick") else None
entry["public_semi"] = (not entry["public_hit"] and wp.get("public_second_pick") == actual) if wp.get("public_pick") else None
# Card analysis
card_values = _card_value_distribution(cards_data)
@@ -1419,7 +1438,7 @@ def get_prediction_analysis() -> dict:
return {
"total_games": len(winners),
"last_winners": winners[-10:] if len(winners) >= 10 else winners,
"last_20_predictions": last_20_raw,
"last_20_predictions": last_50_raw,
"prediction": prediction,
"signals": signals,
"markov1": {"matrix": markov1, "counts": {k: dict(v) for k, v in markov1_counts.items()}},
@@ -1461,8 +1480,10 @@ def get_prediction_history(limit: int = 100) -> dict:
# Merge and compute accuracy
whale_hits = 0
whale_semi_hits = 0
whale_total = 0
public_hits = 0
public_semi_hits = 0
public_total = 0
model_full_hits = 0
model_semi_hits = 0
@@ -1471,11 +1492,15 @@ def get_prediction_history(limit: int = 100) -> dict:
gno = entry.get("game_no", 0)
wp = wp_data.get(gno, {})
entry["whale_pick"] = wp.get("whale_pick")
entry["whale_second_pick"] = wp.get("whale_second_pick")
entry["public_pick"] = wp.get("public_pick")
entry["public_second_pick"] = wp.get("public_second_pick")
entry["bettor_counts"] = wp.get("bettor_counts", {"A": 0, "B": 0, "C": 0})
actual = entry["actual"]
entry["whale_hit"] = (wp.get("whale_pick") == actual) if wp.get("whale_pick") else None
entry["whale_semi"] = (not entry["whale_hit"] and wp.get("whale_second_pick") == actual) if wp.get("whale_pick") else None
entry["public_hit"] = (wp.get("public_pick") == actual) if wp.get("public_pick") else None
entry["public_semi"] = (not entry["public_hit"] and wp.get("public_second_pick") == actual) if wp.get("public_pick") else None
# Remove internal index from output
del entry["index"]
@@ -1489,13 +1514,19 @@ def get_prediction_history(limit: int = 100) -> dict:
whale_total += 1
if entry["whale_hit"]:
whale_hits += 1
elif entry.get("whale_semi"):
whale_semi_hits += 1
if entry["public_hit"] is not None:
public_total += 1
if entry["public_hit"]:
public_hits += 1
elif entry.get("public_semi"):
public_semi_hits += 1
total_pred = len(predictions)
model_score = model_full_hits + model_semi_hits * 0.5
whale_score = whale_hits + whale_semi_hits * 0.5
public_score = public_hits + public_semi_hits * 0.5
return {
"total_games": len(winners),
@@ -1510,13 +1541,15 @@ def get_prediction_history(limit: int = 100) -> dict:
},
"whale": {
"hits": whale_hits,
"semi": whale_semi_hits,
"total": whale_total,
"pct": round(whale_hits / whale_total * 100, 1) if whale_total else 0,
"pct": round(whale_score / whale_total * 100, 1) if whale_total else 0,
},
"public": {
"hits": public_hits,
"semi": public_semi_hits,
"total": public_total,
"pct": round(public_hits / public_total * 100, 1) if public_total else 0,
"pct": round(public_score / public_total * 100, 1) if public_total else 0,
},
},
}

View File

@@ -257,10 +257,13 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
<div id="crowd-stats" class="crowd-stats"></div>
</div>
<!-- Last 20 Predictions vs Actual -->
<!-- Last 50 Predictions vs Actual -->
<div class="section">
<div class="section-title">Last 20 Predictions vs Actual</div>
<div class="panel" style="max-height:500px;overflow-y:auto">
<div class="section-title" style="display:flex;align-items:center;gap:12px">
Last 50 Predictions vs Actual
<button id="view-all-btn" onclick="openHistoryModal()" style="font-size:11px;padding:3px 12px;border-radius:12px;border:1px solid var(--accent);background:transparent;color:var(--accent);cursor:pointer;font-weight:600;text-transform:uppercase;letter-spacing:0.3px">View All History</button>
</div>
<div class="panel" style="max-height:600px;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>Whale</th><th>Public</th><th>Actual</th><th>Result</th></tr></thead>
<tbody></tbody>
@@ -268,6 +271,25 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
</div>
</div>
<!-- Full History Modal -->
<div id="history-modal" style="display:none;position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,0.7);backdrop-filter:blur(4px)">
<div style="position:absolute;inset:20px;background:var(--bg);border:1px solid var(--border);border-radius:12px;display:flex;flex-direction:column;overflow:hidden">
<div style="display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--border);background:var(--surface)">
<div style="font-size:15px;font-weight:700">Full Prediction History</div>
<button onclick="closeHistoryModal()" style="background:none;border:none;color:var(--text2);font-size:22px;cursor:pointer;padding:0 6px;line-height:1">&times;</button>
</div>
<div id="modal-accuracy" style="padding:12px 20px;border-bottom:1px solid var(--border);background:var(--surface2);font-size:12px;display:flex;gap:24px;flex-wrap:wrap"></div>
<div style="flex:1;overflow-y:auto;padding:0 20px">
<table class="pred-history" id="modal-table">
<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>
<div id="modal-pagination" style="display:flex;align-items:center;justify-content:center;gap:16px;padding:12px 20px;border-top:1px solid var(--border);background:var(--surface)">
</div>
</div>
</div>
<!-- Markov Matrices -->
<div class="section">
<div class="section-title">Markov Transition Matrices</div>
@@ -663,44 +685,31 @@ function renderCrowdStats(data) {
`;
}
function renderLast20(predictions) {
const tbody = $('pred-history').querySelector('tbody');
function renderPredictionRows(predictions, tbody) {
if (!predictions || predictions.length === 0) {
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
// Whale cell with semi-win
let whaleCell;
if (p.whale_pick) {
const whCls = p.whale_hit ? 'correct' : 'wrong';
const whLabel = p.whale_hit ? 'HIT' : 'MISS';
const whCls = p.whale_hit ? 'correct' : (p.whale_semi ? 'semi' : 'wrong');
const whLabel = p.whale_hit ? 'HIT' : (p.whale_semi ? 'SEMI' : '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
// Public cell with semi-win
let pubCell;
if (p.public_pick) {
const puCls = p.public_hit ? 'correct' : 'wrong';
const puLabel = p.public_hit ? 'HIT' : 'MISS';
const puCls = p.public_hit ? 'correct' : (p.public_semi ? 'semi' : 'wrong');
const puLabel = p.public_hit ? 'HIT' : (p.public_semi ? 'SEMI' : '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>';
@@ -719,17 +728,114 @@ function renderLast20(predictions) {
<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">
}).join('');
}
function computeAccuracy(predictions) {
const fullHits = predictions.filter(p => p.correct).length;
const semiHits = predictions.filter(p => p.semi_correct).length;
const score = fullHits + semiHits * 0.5;
const whaleEntries = predictions.filter(p => p.whale_pick != null);
const whaleHits = whaleEntries.filter(p => p.whale_hit).length;
const whaleSemi = whaleEntries.filter(p => p.whale_semi).length;
const whaleScore = whaleHits + whaleSemi * 0.5;
const whalePct = whaleEntries.length > 0 ? (whaleScore / whaleEntries.length * 100).toFixed(1) : '--';
const publicEntries = predictions.filter(p => p.public_pick != null);
const publicHits = publicEntries.filter(p => p.public_hit).length;
const publicSemi = publicEntries.filter(p => p.public_semi).length;
const publicScore = publicHits + publicSemi * 0.5;
const publicPct = publicEntries.length > 0 ? (publicScore / publicEntries.length * 100).toFixed(1) : '--';
const modelPct = predictions.length > 0 ? (score / predictions.length * 100).toFixed(1) : '--';
return { score, fullHits, semiHits, whaleHits, whaleSemi, whaleScore, whalePct, whaleTotal: whaleEntries.length,
publicHits, publicSemi, publicScore, publicPct, publicTotal: publicEntries.length, modelPct, total: predictions.length };
}
function renderLast20(predictions) {
const tbody = $('pred-history').querySelector('tbody');
if (!predictions || predictions.length === 0) {
tbody.innerHTML = '<tr><td colspan="10" style="color:var(--text3)">Not enough data</td></tr>';
return;
}
const acc = computeAccuracy(predictions);
renderPredictionRows(predictions, tbody);
// Append accuracy row
tbody.innerHTML += `<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)'}">
Model: ${(score/predictions.length*100).toFixed(1)}%
<td style="color:${acc.whalePct !== '--' && parseFloat(acc.whalePct) > 33.3 ? 'var(--green)' : 'var(--text2)'}">${acc.whalePct !== '--' ? acc.whalePct + '%' : '--'}</td>
<td style="color:${acc.publicPct !== '--' && parseFloat(acc.publicPct) > 33.3 ? 'var(--green)' : 'var(--text2)'}">${acc.publicPct !== '--' ? acc.publicPct + '%' : '--'}</td>
<td colspan="2" style="color:${parseFloat(acc.modelPct) > 33.3 ? 'var(--green)' : 'var(--red)'}">
Model: ${acc.modelPct}%
</td>
</tr>`;
}
// ── Full History Modal ──
let historyCache = null;
let historyPage = 1;
const ROWS_PER_PAGE = 50;
function openHistoryModal() {
$('history-modal').style.display = 'block';
if (historyCache) { renderHistoryPage(1); return; }
$('modal-table').querySelector('tbody').innerHTML = '<tr><td colspan="10" style="color:var(--text3)">Loading...</td></tr>';
$('modal-pagination').innerHTML = '';
$('modal-accuracy').innerHTML = '';
fetch('/api/prediction-history?limit=500').then(r => r.json()).then(data => {
historyCache = data;
renderHistoryPage(1);
}).catch(err => {
$('modal-table').querySelector('tbody').innerHTML = `<tr><td colspan="10" style="color:var(--red)">Failed: ${err.message}</td></tr>`;
});
}
function closeHistoryModal() {
$('history-modal').style.display = 'none';
}
// Close on overlay click
$('history-modal')?.addEventListener('click', function(e) {
if (e.target === this) closeHistoryModal();
});
function renderHistoryPage(page) {
if (!historyCache) return;
const preds = historyCache.predictions || [];
const totalPages = Math.max(1, Math.ceil(preds.length / ROWS_PER_PAGE));
page = Math.max(1, Math.min(page, totalPages));
historyPage = page;
// Accuracy summary bar
const accData = historyCache.accuracy;
$('modal-accuracy').innerHTML = `
<span><b>Model:</b> <span style="color:var(--green)">${accData.model.pct}%</span> <span style="color:var(--text3)">(${accData.model.hits} hits + ${accData.model.semi} semi / ${accData.model.total})</span></span>
<span><b>Whale:</b> <span style="color:${accData.whale.pct > 33.3 ? 'var(--green)' : 'var(--text2)'}">${accData.whale.pct}%</span> <span style="color:var(--text3)">(${accData.whale.hits} hits${accData.whale.semi ? ' + ' + accData.whale.semi + ' semi' : ''} / ${accData.whale.total})</span></span>
<span><b>Public:</b> <span style="color:${accData.public.pct > 33.3 ? 'var(--green)' : 'var(--text2)'}">${accData.public.pct}%</span> <span style="color:var(--text3)">(${accData.public.hits} hits${accData.public.semi ? ' + ' + accData.public.semi + ' semi' : ''} / ${accData.public.total})</span></span>
<span style="color:var(--text3)">${preds.length} predictions total</span>
`;
// Slice for current page (preds are oldest-first, show newest-first)
const reversed = preds.slice().reverse();
const start = (page - 1) * ROWS_PER_PAGE;
const pageData = reversed.slice(start, start + ROWS_PER_PAGE);
const tbody = $('modal-table').querySelector('tbody');
// Reverse pageData back so renderPredictionRows (which reverses internally) shows correct order
renderPredictionRows(pageData.slice().reverse(), tbody);
// Pagination controls
$('modal-pagination').innerHTML = `
<button onclick="renderHistoryPage(${page - 1})" style="padding:6px 14px;border-radius:6px;border:1px solid var(--border);background:var(--surface2);color:var(--text);cursor:pointer;font-size:12px" ${page <= 1 ? 'disabled style="opacity:0.4;padding:6px 14px;border-radius:6px;border:1px solid var(--border);background:var(--surface2);color:var(--text);cursor:default;font-size:12px"' : ''}>&#8592; Prev</button>
<span style="font-size:12px;color:var(--text2)">Page ${page} of ${totalPages}</span>
<button onclick="renderHistoryPage(${page + 1})" style="padding:6px 14px;border-radius:6px;border:1px solid var(--border);background:var(--surface2);color:var(--text);cursor:pointer;font-size:12px" ${page >= totalPages ? 'disabled style="opacity:0.4;padding:6px 14px;border-radius:6px;border:1px solid var(--border);background:var(--surface2);color:var(--text);cursor:default;font-size:12px"' : ''}>Next &#8594;</button>
`;
}
function renderMarkov1(m) {
const t = $('markov1-table');
let html = '<tr><th>From \\ To</th>' + CHAIRS.map(c => `<th style="color:${CHAIR_COLORS[c]}">\u2192${c}</th>`).join('') + '</tr>';
@@ -1020,6 +1126,7 @@ function onRoundResult(data) {
(extra.length ? ` &nbsp;\u00b7&nbsp; ${extra.join(' &nbsp;\u00b7&nbsp; ')}` : '');
}
historyCache = null; // invalidate so modal fetches fresh data
setTimeout(refreshPredictions, 2000);
}