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"):
|
||||
|
||||
Reference in New Issue
Block a user