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):
"""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,
},
},
}

View File

@@ -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"):