"""Enriches site/data/leaderboard.json: risk-adjusted Arena Score, risk badges, sparklines, rank arrows,
weekly seasons + champions, hall of fame, head-to-head rivalries. Paper trading only. Not financial advice.

Arena Score (0-100ish, higher is better) rewards return per unit of risk so gambling doesn't win:
  score = 50 + 20*clip(Sharpe, -2.5, 2.5) + 50*total_return - 60*|max_drawdown|
A big return with a deep drawdown scores worse than a steady one."""
import json, os, math, datetime as dt, itertools
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))); P = os.path.join(ROOT, "site", "data", "leaderboard.json")
D = json.load(open(P))
clip = lambda x, a, b: max(a, min(b, x))
def stats(vals):
    r = [vals[i] / vals[i-1] - 1 for i in range(1, len(vals))] or [0]
    mu = sum(r) / len(r); sd = math.sqrt(sum((x-mu)**2 for x in r) / len(r)) or 1e-9
    pk, mdd = vals[0], 0
    for v in vals: pk = max(pk, v); mdd = min(mdd, v / pk - 1)
    return vals[-1] / vals[0] - 1, mdd, mu / sd * math.sqrt(252), sd * math.sqrt(252)
score = lambda tr, mdd, sh: round(50 + 20 * clip(sh, -2.5, 2.5) + 50 * tr - 60 * abs(mdd), 1)
def badges(a, vol):
    b = []
    m = abs(a["max_drawdown"])
    b.append({"id": "risk-high", "label": "High risk", "tone": "red"} if m > 0.30 else {"id": "risk-med", "label": "Medium risk", "tone": "amber"} if m > 0.15 else {"id": "risk-low", "label": "Low risk", "tone": "green"})
    if a["holdings"] and max(a["holdings"].values()) > 0.6: b.append({"id": "concentrated", "label": "Concentrated", "tone": "amber"})
    if vol > 0.6: b.append({"id": "volatile", "label": "Volatile", "tone": "red"})
    if a["sharpe"] > 1.5 and m < 0.15: b.append({"id": "smooth", "label": "Smooth operator", "tone": "green"})
    if a["trades"] > 500: b.append({"id": "churn", "label": "Heavy churn", "tone": "amber"})
    if a["income"] > 15000: b.append({"id": "income", "label": "Income machine", "tone": "violet"})
    return b
D["paused"] = []  # no hidden list any more: retired bots stay on the boards
D["agents"] = A = D["agents"]  # bots never disappear: retired bots stay listed (marked retired) and count in league stats
for a in A:
    v = [p["value"] for p in a["curve"]]
    tr, mdd, sh, vol = stats(v)
    a["score"] = score(tr, mdd, sh); a["vol"] = round(vol, 4); a["badges"] = badges(a, vol)
    a["spark"] = [round(x / v[-30:][0] * 100, 2) for x in v[-30:]]
    v5 = v[:-5] or v[:1]; tr5, mdd5, sh5, _ = stats(v5); a["score_5d_ago"] = score(tr5, mdd5, sh5)
def rank(lst, key):
    for i, a in enumerate(sorted(lst, key=lambda x: -x[key])): a.setdefault("_r", {})[key] = i + 1
rank(A, "score"); rank(A, "score_5d_ago")
for lid in D["leagues"]:
    L = [a for a in A if a["league"] == lid]
    for i, a in enumerate(sorted(L, key=lambda x: -x["score"])): a["league_rank"] = i + 1
    for i, a in enumerate(sorted(L, key=lambda x: -x["score_5d_ago"])): a["league_rank_prev"] = i + 1
for a in A:
    a["rank"] = a["_r"]["score"]; a["rank_prev"] = a["_r"]["score_5d_ago"]; a["rank_change"] = a["rank_prev"] - a["rank"]; del a["_r"]
A.sort(key=lambda x: x["rank"])
# --- presentation: asset types (labels/filters) + tiers (Premier League, Division 1, Division 2, Division 3, Division 4, Qualifying). Ranked by total paper value only.
# League ids, bot ids and records are unchanged; nothing is deleted.
ASSET_OF = {"income": "Income ETFs", "etfs": "Income ETFs", "crypto_l1": "Crypto", "crypto_l2": "Crypto", "crypto_l3": "Crypto", "crypto_alt": "Crypto",
            "tokenized": "US Stocks", "us_stocks": "US Stocks", "tsx_stocks": "TSX Stocks", "multi": "Multi-Assets"}
D["asset_types"] = {"Income ETFs": "ETFs, including high-income dividend ETFs", "Crypto": "Spot crypto only (Layer 1/2/3 and altcoins), no leverage",
                    "US Stocks": "US-listed and tokenized US stocks", "TSX Stocks": "Toronto Stock Exchange stocks",
                    "Multi-Assets": "A bot trading across any of the other asset types in one portfolio"}
_DC = json.load(open(os.path.join(ROOT, "config.json"))).get("divisions", {})
D["division_config"] = {k: _DC[k] for k in ("target_size", "promotion_pct", "relegation_pct", "parallel_divisions", "qualifying_capped") if k in _DC}
D["tiers"] = ["Premier League", "Division 1", "Division 2", "Division 3", "Division 4", "Qualifying"]
D.pop("divisions", None)
# Season 1 seeding (by asset type, not performance; every bot starts equal at $100k): within each market, house bots sorted by
# strategy id; the first half go to the Premier League, the rest to Division 1. Every outside bot starts in Qualifying.
for lid in D["leagues"]:
    hs = sorted([a for a in A if a["league"] == lid and (a.get("house") or a.get("author", "house") == "house")], key=lambda x: x["base_id"])
    for k, a in enumerate(hs): a["tier"] = "Premier League" if k < (len(hs) + 1) // 2 else "Division 1"
for a in A:
    a["asset_type"] = ASSET_OF.get(a["league"], "Multi-Assets"); a.setdefault("tier", None)
    if a["tier"] not in D["tiers"]: a["tier"] = "Qualifying"
    a["paper_value_usd"] = round(a["final_value"], 2); a["paper_gain_usd"] = round(a["final_value"] - 100000, 2)
    c = a["curve"]; a["prev_value"] = c[-2]["value"] if len(c) >= 2 else None
    for k in ("division", "division_rank", "premier_zone", "capital_rank"): a.pop(k, None)
def _rank(lst, key, out):
    for i, a in enumerate(sorted(lst, key=lambda x: (-(x[key] if x[key] is not None else x["final_value"]), x["id"]))): a[out] = i + 1
for t in D["tiers"]:
    T = [a for a in A if a["tier"] == t]; _rank(T, "final_value", "tier_rank"); _rank(T, "prev_value", "tier_rank_prev")
_rank(A, "final_value", "value_rank"); _rank(A, "prev_value", "value_rank_prev")
for a in A:
    h = a["prev_value"] is not None
    a["tier_move"] = (a["tier_rank_prev"] - a["tier_rank"]) if h else None; a["value_move"] = (a["value_rank_prev"] - a["value_rank"]) if h else None
# --- weekly seasons (ISO week, Mon-Fri) ---
weeks = {}
for i, p in enumerate(A[0]["curve"]):
    y, w, _ = dt.date.fromisoformat(p["date"]).isocalendar(); weeks.setdefault(f"{y}-W{w:02d}", []).append(i)
wk_ids = sorted(weeks)
seasons, hof = [], {}
for wk in wk_ids:
    idx = weeks[wk]; s0 = max(0, idx[0] - 1); s1 = idx[-1]
    entry = {"season": wk, "start": A[0]["curve"][idx[0]]["date"], "end": A[0]["curve"][s1]["date"], "champions": {}}
    for lid in D["leagues"]:
        best = None
        for a in [x for x in A if x["league"] == lid]:
            v = [p["value"] for p in a["curve"][s0:s1 + 1]]
            if len(v) < 2: continue
            tr, mdd, sh, _ = stats(v); sc = round(50 + 20 * clip(sh / math.sqrt(52), -2.5, 2.5) + 50 * tr - 60 * abs(mdd), 1)
            if not best or sc > best["score"]: best = {"agent": a["id"], "name": a["name"], "score": sc, "return": round(tr, 4)}
        if best:
            entry["champions"][lid] = best; h = hof.setdefault(best["agent"], {"agent": best["agent"], "name": best["name"], "league": lid, "titles": 0, "weeks": []}); h["titles"] += 1; h["weeks"].append(wk)
    seasons.append(entry)
D["current_season"] = seasons[-1]; D["seasons"] = seasons[::-1]
D["hall_of_fame"] = sorted(hof.values(), key=lambda h: -h["titles"])
# --- rivalries: closest pairs by score within each league, with head-to-head weekly record ---
riv = []
for lid in D["leagues"]:
    L = sorted([a for a in A if a["league"] == lid], key=lambda x: -x["score"])
    for x, y in zip(L, L[1:]):
        wx = wy = 0
        for wk in wk_ids:
            idx = weeks[wk]; s0 = max(0, idx[0] - 1); s1 = idx[-1]
            rx = x["curve"][s1]["value"] / x["curve"][s0]["value"]; ry = y["curve"][s1]["value"] / y["curve"][s0]["value"]
            wx += rx > ry; wy += ry > rx
        riv.append({"league": lid, "a": x["id"], "a_name": x["name"], "b": y["id"], "b_name": y["name"], "gap": round(x["score"] - y["score"], 1), "record": [wx, wy]})
D["rivalries"] = sorted(riv, key=lambda r: r["gap"])[:8]
D["stats"] = {"agents": len(A), "leagues": len(D["leagues"]), "symbols": len(D["funds"]), "paper_capital_usd": 100000 * len(A),
              "trades": sum(a["trades"] for a in A), "income_usd": round(sum(a["income"] for a in A)), "seasons": len(seasons), "counts": "live record only",
              "backtest_trades": D.get("backtest_totals", {}).get("trades", 0), "backtest_income_usd": D.get("backtest_totals", {}).get("income_usd", 0)}
D["score_formula"] = "50 + 20×Sharpe(clipped ±2.5) + 50×total return − 60×|max drawdown|"
# compact payload for the board (curves only kept for detail pages)
json.dump(D, open(P, "w"))
print("analytics:", len(A), "agents,", len(seasons), "weekly seasons, top:", A[0]["name"], A[0]["league"], A[0]["score"])
