@@ -0,0 +1,253 @@
|
||||
"""
|
||||
Live Scoreboard — the single source of truth for real betting performance.
|
||||
=========================================================================
|
||||
Reads the *forward-tracked* results in `prediction_runs` (one row per analyzed
|
||||
match, with the staked main pick + actual outcome + realized unit_profit) and
|
||||
reports what ACTUALLY happened with real money logic — NOT a backtest.
|
||||
|
||||
Why this exists: backtests on this codebase are overfit (a paper "+32.7% ROI"
|
||||
strategy that the live engine never even ran). The only trustworthy number is
|
||||
the realized P/L recorded after matches settle. This tool surfaces it.
|
||||
|
||||
Read-only. SELECT only. Safe to run anytime.
|
||||
|
||||
Usage:
|
||||
python scripts/live_scoreboard.py
|
||||
python scripts/live_scoreboard.py --days 30
|
||||
python scripts/live_scoreboard.py --version v28-pro-max
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
# utf-8 stdout so Turkish market/league names never crash on Windows cp1252
|
||||
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
AI_ENGINE_DIR = os.path.dirname(SCRIPT_DIR)
|
||||
sys.path.insert(0, AI_ENGINE_DIR)
|
||||
|
||||
from data.db import get_clean_dsn # noqa: E402
|
||||
import psycopg2 # noqa: E402
|
||||
from psycopg2.extras import RealDictCursor # noqa: E402
|
||||
|
||||
ODDS_BANDS = [(0, 1.5, "<1.5"), (1.5, 2.0, "1.5-2"), (2.0, 3.0, "2-3"),
|
||||
(3.0, 5.0, "3-5"), (5.0, 6.0, "5-6"), (6.0, 7.5, "6-7.5"),
|
||||
(7.5, 999, "7.5+")]
|
||||
|
||||
|
||||
def _f(x: Any, d: Optional[float] = None) -> Optional[float]:
|
||||
try:
|
||||
return float(x) if x is not None else d
|
||||
except (TypeError, ValueError):
|
||||
return d
|
||||
|
||||
|
||||
def _parse(j: Any) -> Dict[str, Any]:
|
||||
if isinstance(j, str):
|
||||
try:
|
||||
return json.loads(j)
|
||||
except Exception:
|
||||
return {}
|
||||
return j or {}
|
||||
|
||||
|
||||
def _band(odds: Optional[float]) -> str:
|
||||
if odds is None:
|
||||
return "?"
|
||||
for lo, hi, name in ODDS_BANDS:
|
||||
if lo <= odds < hi:
|
||||
return name
|
||||
return "?"
|
||||
|
||||
|
||||
def fetch_rows(args) -> List[Dict[str, Any]]:
|
||||
dsn = get_clean_dsn()
|
||||
where = ["eventual_outcome IS NOT NULL"]
|
||||
params: List[Any] = []
|
||||
if args.version:
|
||||
where.append("engine_version = %s")
|
||||
params.append(args.version)
|
||||
if args.days:
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(days=args.days)
|
||||
where.append("generated_at >= %s")
|
||||
params.append(cutoff)
|
||||
sql = f"""
|
||||
SELECT match_id, engine_version, generated_at, eventual_outcome,
|
||||
unit_profit, payload_summary
|
||||
FROM prediction_runs
|
||||
WHERE {' AND '.join(where)}
|
||||
ORDER BY generated_at ASC
|
||||
"""
|
||||
with psycopg2.connect(dsn) as conn:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(sql, params)
|
||||
return cur.fetchall()
|
||||
|
||||
|
||||
def distill(rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""One analytic record per run with the staked pick + realized P/L."""
|
||||
out = []
|
||||
for r in rows:
|
||||
ps = _parse(r["payload_summary"])
|
||||
mp = ps.get("main_pick") or {}
|
||||
playable = bool(mp.get("playable"))
|
||||
stake = _f(mp.get("stake_units"), 0.0) or 0.0
|
||||
profit = _f(r["unit_profit"], 0.0) or 0.0
|
||||
outcome = str(r["eventual_outcome"] or "")
|
||||
staked = playable and stake > 0
|
||||
# settled stake = a real bet with a win/loss (exclude NO_BET / push)
|
||||
settled_stake = staked and not outcome.startswith(("NO_BET", "PUSH", "VOID", "CANCEL"))
|
||||
out.append({
|
||||
"match_id": r["match_id"],
|
||||
"version": r["engine_version"],
|
||||
"ts": r["generated_at"],
|
||||
"market": mp.get("market") or "?",
|
||||
"pick": mp.get("pick"),
|
||||
"odds": _f(mp.get("odds")),
|
||||
"stake": stake,
|
||||
"profit": profit,
|
||||
"outcome": outcome,
|
||||
"staked": staked,
|
||||
"settled_stake": settled_stake,
|
||||
"win": settled_stake and profit > 0,
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def _agg(recs: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
# NOTE: recorded unit_profit is on a FLAT 1u basis (win=odds-1, loss=-1),
|
||||
# independent of the brain's suggested stake_units. So ROI is profit per
|
||||
# bet at 1u flat = profit / n. (Using stake_units as denominator is wrong:
|
||||
# it double-counts and produces impossible >100% losses.)
|
||||
s = [r for r in recs if r["settled_stake"]]
|
||||
n = len(s)
|
||||
wins = sum(1 for r in s if r["win"])
|
||||
sug_stake = sum(r["stake"] for r in s)
|
||||
profit = sum(r["profit"] for r in s)
|
||||
return {
|
||||
"n": n,
|
||||
"wins": wins,
|
||||
"hit_pct": round(100.0 * wins / n, 1) if n else None,
|
||||
"sug_stake": round(sug_stake, 2),
|
||||
"profit": round(profit, 2),
|
||||
"roi_pct": round(100.0 * profit / n, 1) if n else None, # flat 1u
|
||||
}
|
||||
|
||||
|
||||
def _line(label: str, a: Dict[str, Any]) -> str:
|
||||
return (f" {label:<14} n={a['n']:>4} hit={str(a['hit_pct'] if a['hit_pct'] is not None else '-'):>5}% "
|
||||
f"profit={a['profit']:>8.2f}u ROI(flat1u)={str(a['roi_pct'] if a['roi_pct'] is not None else '-'):>7}%")
|
||||
|
||||
|
||||
def risk_metrics(recs: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
s = [r for r in sorted(recs, key=lambda x: x["ts"]) if r["settled_stake"]]
|
||||
cum = 0.0
|
||||
peak = 0.0
|
||||
max_dd = 0.0
|
||||
streak = 0
|
||||
worst_streak = 0
|
||||
for r in s:
|
||||
cum += r["profit"]
|
||||
peak = max(peak, cum)
|
||||
max_dd = min(max_dd, cum - peak)
|
||||
if r["profit"] <= 0:
|
||||
streak += 1
|
||||
worst_streak = max(worst_streak, streak)
|
||||
else:
|
||||
streak = 0
|
||||
return {"max_drawdown_u": round(max_dd, 2),
|
||||
"longest_losing_streak": worst_streak,
|
||||
"final_cum_u": round(cum, 2)}
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--days", type=int, default=None, help="Only last N days")
|
||||
ap.add_argument("--version", help="Filter by engine_version")
|
||||
args = ap.parse_args()
|
||||
|
||||
rows = fetch_rows(args)
|
||||
recs = distill(rows)
|
||||
|
||||
print("=" * 74)
|
||||
print("LIVE SCOREBOARD — realized results from prediction_runs (NOT backtest)")
|
||||
print("=" * 74)
|
||||
if recs:
|
||||
lo = min(r["ts"] for r in recs).date()
|
||||
hi = max(r["ts"] for r in recs).date()
|
||||
print(f"window: {lo} .. {hi} settled runs: {len(recs)}"
|
||||
+ (f" filter: {args.version}" if args.version else ""))
|
||||
print()
|
||||
|
||||
overall = _agg(recs)
|
||||
print("OVERALL (staked = playable bets only)")
|
||||
print(_line("ALL", overall))
|
||||
no_bet = sum(1 for r in recs if not r["staked"])
|
||||
print(f" (analyzed {len(recs)} matches; {overall['n']} actually staked, "
|
||||
f"{no_bet} NO_BET)")
|
||||
if overall["n"]:
|
||||
rm = risk_metrics(recs)
|
||||
print(f" max drawdown: {rm['max_drawdown_u']}u "
|
||||
f"longest losing streak: {rm['longest_losing_streak']} "
|
||||
f"net: {rm['final_cum_u']}u")
|
||||
print()
|
||||
|
||||
print("BY ENGINE VERSION")
|
||||
by_v = defaultdict(list)
|
||||
for r in recs:
|
||||
by_v[r["version"]].append(r)
|
||||
for v, rs in sorted(by_v.items(), key=lambda kv: -len(kv[1])):
|
||||
print(_line(v, _agg(rs)))
|
||||
print()
|
||||
|
||||
print("BY MARKET (staked)")
|
||||
by_m = defaultdict(list)
|
||||
for r in recs:
|
||||
if r["settled_stake"]:
|
||||
by_m[r["market"]].append(r)
|
||||
for m, rs in sorted(by_m.items(), key=lambda kv: -len(kv[1])):
|
||||
print(_line(m, _agg(rs)))
|
||||
if not by_m:
|
||||
print(" (no staked settled bets in window)")
|
||||
print()
|
||||
|
||||
print("BY ODDS BAND (staked)")
|
||||
by_b = defaultdict(list)
|
||||
for r in recs:
|
||||
if r["settled_stake"]:
|
||||
by_b[_band(r["odds"])].append(r)
|
||||
for _, _, name in ODDS_BANDS:
|
||||
if name in by_b:
|
||||
print(_line(name, _agg(by_b[name])))
|
||||
print()
|
||||
|
||||
print("WEEKLY TREND (staked)")
|
||||
by_w = defaultdict(list)
|
||||
for r in recs:
|
||||
if r["settled_stake"]:
|
||||
iso = r["ts"].isocalendar()
|
||||
by_w[f"{iso[0]}-W{iso[1]:02d}"].append(r)
|
||||
for w in sorted(by_w):
|
||||
a = _agg(by_w[w])
|
||||
print(_line(w, a))
|
||||
print()
|
||||
print("=" * 74)
|
||||
print("READ: ROI < 0 over a meaningful sample = the staked signals are not")
|
||||
print("profitable. 'NO_BET' rows are free (no stake). CLV is unmeasurable")
|
||||
print("until odds movement is captured (see scripts + odds_history fix).")
|
||||
print("=" * 74)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user