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:
178
app/db.py
178
app/db.py
@@ -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):
|
||||
"""Get detailed prediction vs actual for the last N games."""
|
||||
warmup = 30
|
||||
@@ -1300,6 +1379,19 @@ def get_prediction_analysis() -> dict:
|
||||
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:
|
||||
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_values = _card_value_distribution(cards_data)
|
||||
face_cards = _face_card_frequency(cards_data)
|
||||
@@ -1342,3 +1434,89 @@ def get_prediction_analysis() -> dict:
|
||||
"suits": suits,
|
||||
"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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ class WebServer:
|
||||
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("/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_static("/static/", STATIC_DIR, name="static")
|
||||
|
||||
@@ -118,6 +119,15 @@ class WebServer:
|
||||
log.error("Prediction analysis query failed: %s", e)
|
||||
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:
|
||||
period = request.query.get("period", "all")
|
||||
if period not in ("1h", "6h", "24h", "7d", "all"):
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user