diff --git a/app/db.py b/app/db.py index 82a0e4bab..49871418a 100644 --- a/app/db.py +++ b/app/db.py @@ -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, }, }, } diff --git a/static/predictions.html b/static/predictions.html index e2bc18dec..c8292b806 100644 --- a/static/predictions.html +++ b/static/predictions.html @@ -257,10 +257,13 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
- +
-
Last 20 Predictions vs Actual
-
+
+ Last 50 Predictions vs Actual + +
+
@@ -268,6 +271,25 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans- + +
GamePredicted2nd PickP(A)P(B)P(C)WhalePublicActualResult
+ + + +
+ +
+
+
Markov Transition Matrices
@@ -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 = 'Not enough data'; 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 = `${p.whale_pick} ${whLabel}`; } else { whaleCell = '--'; } - // 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 = `${p.public_pick} ${puLabel}`; } else { pubCell = '--'; @@ -719,17 +728,114 @@ function renderLast20(predictions) { ${p.actual} ${resultLabel} `; - }).join('') + - ` + }).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 = 'Not enough data'; + return; + } + const acc = computeAccuracy(predictions); + + renderPredictionRows(predictions, tbody); + + // Append accuracy row + tbody.innerHTML += ` Accuracy (last ${predictions.length}) - ${whalePct !== '--' ? whalePct + '%' : '--'} - ${publicPct !== '--' ? publicPct + '%' : '--'} - - Model: ${(score/predictions.length*100).toFixed(1)}% + ${acc.whalePct !== '--' ? acc.whalePct + '%' : '--'} + ${acc.publicPct !== '--' ? acc.publicPct + '%' : '--'} + + Model: ${acc.modelPct}% `; } +// ── 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 = 'Loading...'; + $('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 = `Failed: ${err.message}`; + }); +} + +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 = ` + Model: ${accData.model.pct}% (${accData.model.hits} hits + ${accData.model.semi} semi / ${accData.model.total}) + Whale: ${accData.whale.pct}% (${accData.whale.hits} hits${accData.whale.semi ? ' + ' + accData.whale.semi + ' semi' : ''} / ${accData.whale.total}) + Public: ${accData.public.pct}% (${accData.public.hits} hits${accData.public.semi ? ' + ' + accData.public.semi + ' semi' : ''} / ${accData.public.total}) + ${preds.length} predictions total + `; + + // 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 = ` + + Page ${page} of ${totalPages} + + `; +} + function renderMarkov1(m) { const t = $('markov1-table'); let html = 'From \\ To' + CHAIRS.map(c => `\u2192${c}`).join('') + ''; @@ -1020,6 +1126,7 @@ function onRoundResult(data) { (extra.length ? `  \u00b7  ${extra.join('  \u00b7  ')}` : ''); } + historyCache = null; // invalidate so modal fetches fresh data setTimeout(refreshPredictions, 2000); }