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

178
app/db.py
View File

@@ -1229,6 +1229,85 @@ def _backtest_theories(winners):
} }
def _compute_whale_public_picks(client, game_nos):
"""Compute whale pick and public pick for a list of game_nos from bets data.
Returns {game_no: {whale_pick, public_pick, whale_count, public_count, bettor_counts}}.
"""
if not game_nos:
return {}
game_nos_csv = ",".join(str(g) for g in game_nos)
result = client.query(
f"SELECT game_no, user_id, chair, sum(bet_amount) AS total_user_bet "
f"FROM bets WHERE game_no IN ({game_nos_csv}) "
f"GROUP BY game_no, user_id, chair"
)
# Collect per-game, per-user, per-chair totals
# game_data[game_no][user_id][chair] = amount
game_data = {}
for row in result.result_rows:
gno, uid, chair_id, amount = row[0], row[1], row[2], row[3]
if gno not in game_data:
game_data[gno] = {}
if uid not in game_data[gno]:
game_data[gno][uid] = {}
chair_name = config.CHAIRS.get(chair_id, "?")
if chair_name in CHAIR_LABELS:
game_data[gno][uid][chair_name] = game_data[gno][uid].get(chair_name, 0) + amount
picks = {}
for gno in game_nos:
users = game_data.get(gno, {})
if not users:
picks[gno] = {"whale_pick": None, "public_pick": None,
"whale_count": 0, "public_count": 0,
"bettor_counts": {"A": 0, "B": 0, "C": 0}}
continue
# Rank users by total bet across all chairs
user_totals = []
for uid, chairs in users.items():
user_totals.append((uid, sum(chairs.values())))
user_totals.sort(key=lambda x: x[1], reverse=True)
whale_uids = set(uid for uid, _ in user_totals[:5])
pub_uids = set(uid for uid, _ in user_totals[5:])
# Sum whale money per chair
whale_money = {"A": 0, "B": 0, "C": 0}
for uid in whale_uids:
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
# Sum public money per chair
pub_money = {"A": 0, "B": 0, "C": 0}
for uid in pub_uids:
for c in CHAIR_LABELS:
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
# Count bettors per chair
bettor_counts = {"A": 0, "B": 0, "C": 0}
for uid, chairs in users.items():
for c in CHAIR_LABELS:
if chairs.get(c, 0) > 0:
bettor_counts[c] += 1
picks[gno] = {
"whale_pick": whale_pick,
"public_pick": public_pick,
"whale_count": len(whale_uids),
"public_count": len(pub_uids),
"bettor_counts": bettor_counts,
}
return picks
def _last_n_predictions(winners, n=20): def _last_n_predictions(winners, n=20):
"""Get detailed prediction vs actual for the last N games.""" """Get detailed prediction vs actual for the last N games."""
warmup = 30 warmup = 30
@@ -1300,6 +1379,19 @@ def get_prediction_analysis() -> dict:
idx = entry["index"] idx = entry["index"]
entry["game_no"] = game_nos[idx] if idx < len(game_nos) else 0 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:
gno = entry.get("game_no", 0)
wp = wp_data.get(gno, {})
entry["whale_pick"] = wp.get("whale_pick")
entry["public_pick"] = wp.get("public_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["public_hit"] = (wp.get("public_pick") == actual) if wp.get("public_pick") else None
# Card analysis # Card analysis
card_values = _card_value_distribution(cards_data) card_values = _card_value_distribution(cards_data)
face_cards = _face_card_frequency(cards_data) face_cards = _face_card_frequency(cards_data)
@@ -1342,3 +1434,89 @@ def get_prediction_analysis() -> dict:
"suits": suits, "suits": suits,
"winning_cards": winning_cards, "winning_cards": winning_cards,
} }
@_with_lock
def get_prediction_history(limit: int = 100) -> dict:
"""Get prediction history with whale/public picks and accuracy summary."""
client = get_client()
# Full winner sequence with game numbers
result = client.query("SELECT game_no, winner FROM games ORDER BY game_no ASC")
game_nos = [r[0] for r in result.result_rows if config.CHAIRS.get(r[1], "?") in CHAIR_LABELS]
winners = [config.CHAIRS.get(r[1], "?") for r in result.result_rows]
winners = [w for w in winners if w in CHAIR_LABELS]
# Get last N predictions
predictions = _last_n_predictions(winners, limit)
# Attach game_nos
for entry in predictions:
idx = entry["index"]
entry["game_no"] = game_nos[idx] if idx < len(game_nos) else 0
# Compute whale/public picks
pred_game_nos = [e["game_no"] for e in predictions if e.get("game_no")]
wp_data = _compute_whale_public_picks(client, pred_game_nos)
# Merge and compute accuracy
whale_hits = 0
whale_total = 0
public_hits = 0
public_total = 0
model_full_hits = 0
model_semi_hits = 0
for entry in predictions:
gno = entry.get("game_no", 0)
wp = wp_data.get(gno, {})
entry["whale_pick"] = wp.get("whale_pick")
entry["public_pick"] = wp.get("public_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["public_hit"] = (wp.get("public_pick") == actual) if wp.get("public_pick") else None
# Remove internal index from output
del entry["index"]
# Accumulate accuracy
if entry["correct"]:
model_full_hits += 1
elif entry["semi_correct"]:
model_semi_hits += 1
if entry["whale_hit"] is not None:
whale_total += 1
if entry["whale_hit"]:
whale_hits += 1
if entry["public_hit"] is not None:
public_total += 1
if entry["public_hit"]:
public_hits += 1
total_pred = len(predictions)
model_score = model_full_hits + model_semi_hits * 0.5
return {
"total_games": len(winners),
"limit": limit,
"predictions": predictions,
"accuracy": {
"model": {
"hits": model_full_hits,
"semi": model_semi_hits,
"total": total_pred,
"pct": round(model_score / total_pred * 100, 1) if total_pred else 0,
},
"whale": {
"hits": whale_hits,
"total": whale_total,
"pct": round(whale_hits / whale_total * 100, 1) if whale_total else 0,
},
"public": {
"hits": public_hits,
"total": public_total,
"pct": round(public_hits / public_total * 100, 1) if public_total else 0,
},
},
}

View File

@@ -45,6 +45,7 @@ class WebServer:
self.app.router.add_get("/api/patterns", self._handle_patterns) self.app.router.add_get("/api/patterns", self._handle_patterns)
self.app.router.add_get("/predictions", self._handle_predictions_page) self.app.router.add_get("/predictions", self._handle_predictions_page)
self.app.router.add_get("/api/predictions", self._handle_predictions) self.app.router.add_get("/api/predictions", self._handle_predictions)
self.app.router.add_get("/api/prediction-history", self._handle_prediction_history)
self.app.router.add_get("/ws", self._handle_ws) self.app.router.add_get("/ws", self._handle_ws)
self.app.router.add_static("/static/", STATIC_DIR, name="static") self.app.router.add_static("/static/", STATIC_DIR, name="static")
@@ -118,6 +119,15 @@ class WebServer:
log.error("Prediction analysis query failed: %s", e) log.error("Prediction analysis query failed: %s", e)
return web.json_response({"error": str(e)}, status=500) return web.json_response({"error": str(e)}, status=500)
async def _handle_prediction_history(self, request: web.Request) -> web.Response:
limit = min(int(request.query.get("limit", 100)), 500)
try:
data = await _run_sync(db.get_prediction_history, limit)
return web.json_response(data)
except Exception as e:
log.error("Prediction history query failed: %s", e)
return web.json_response({"error": str(e)}, status=500)
async def _handle_analytics(self, request: web.Request) -> web.Response: async def _handle_analytics(self, request: web.Request) -> web.Response:
period = request.query.get("period", "all") period = request.query.get("period", "all")
if period not in ("1h", "6h", "24h", "7d", "all"): if period not in ("1h", "6h", "24h", "7d", "all"):

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="section-title">Last 20 Predictions vs Actual</div>
<div class="panel" style="max-height:500px;overflow-y:auto"> <div class="panel" style="max-height:500px;overflow-y:auto">
<table class="pred-history" id="pred-history"> <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> <tbody></tbody>
</table> </table>
</div> </div>
@@ -666,16 +666,46 @@ function renderCrowdStats(data) {
function renderLast20(predictions) { function renderLast20(predictions) {
const tbody = $('pred-history').querySelector('tbody'); const tbody = $('pred-history').querySelector('tbody');
if (!predictions || predictions.length === 0) { 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; return;
} }
const fullHits = predictions.filter(p => p.correct).length; const fullHits = predictions.filter(p => p.correct).length;
const semiHits = predictions.filter(p => p.semi_correct).length; const semiHits = predictions.filter(p => p.semi_correct).length;
const score = fullHits + semiHits * 0.5; 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 => { tbody.innerHTML = predictions.slice().reverse().map(p => {
const maxProb = Math.max(p.probs.A, p.probs.B, p.probs.C); const maxProb = Math.max(p.probs.A, p.probs.B, p.probs.C);
const resultClass = p.correct ? 'correct' : (p.semi_correct ? 'semi' : 'wrong'); const resultClass = p.correct ? 'correct' : (p.semi_correct ? 'semi' : 'wrong');
const resultLabel = p.correct ? 'HIT' : (p.semi_correct ? 'SEMI' : 'MISS'); 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> return `<tr>
<td style="font-weight:700">#${p.game_no}</td> <td style="font-weight:700">#${p.game_no}</td>
<td class="winner-cell" style="color:${CHAIR_COLORS[p.predicted]}">${p.predicted}</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; 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>`; return `<td><span class="prob-bar" style="width:${w}px;background:${CHAIR_COLORS[c]}"></span> ${pct(p.probs[c])}</td>`;
}).join('')} }).join('')}
${whaleCell}
${pubCell}
<td class="winner-cell" style="color:${CHAIR_COLORS[p.actual]}">${p.actual}</td> <td class="winner-cell" style="color:${CHAIR_COLORS[p.actual]}">${p.actual}</td>
<td class="${resultClass}" style="font-weight:700">${resultLabel}</td> <td class="${resultClass}" style="font-weight:700">${resultLabel}</td>
</tr>`; </tr>`;
}).join('') + }).join('') +
`<tr style="border-top:2px solid var(--border);font-weight:700"> `<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 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)'}"> <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> </td>
</tr>`; </tr>`;
} }