""" Generate Daily Picks — the serving picker for the validated favourite policy. ============================================================================ Loads the saved leak-free MS model (models/favorite_v1) and applies the favourite-band value policy to a set of matches, emitting the day's STAKED picks and logging them for forward paper-trade settlement. Train/serve consistency: features MUST come from the SAME extractor that built training_data_v27.csv. Production path = run the extractor nightly INCLUDING upcoming (status NS) matches, then point this script at that CSV. Demo path = use the tail of the training CSV as stand-in "today" matches (with the real result shown, since those are settled). Policy: bet the MS side with the biggest model_prob - implied edge, ONLY if odds in [--lo,--hi] and edge>--margin. Flat 1u. No longshots, no parlays. Non-MS markets are NOT staked (efficient -> model error). One bet per match. Usage: python scripts/generate_daily_picks.py --demo --n 20 # see it work now python scripts/generate_daily_picks.py --features today.csv # production python scripts/generate_daily_picks.py --settle # settle paper log """ from __future__ import annotations import argparse, json, os, sys, datetime import numpy as np, pandas as pd, xgboost as xgb if sys.stdout and hasattr(sys.stdout, "reconfigure"): try: sys.stdout.reconfigure(encoding="utf-8") except Exception: pass AI_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) MODEL_DIR = os.path.join(AI_DIR, "models", "favorite_v1") TRAIN_CSV = os.path.join(AI_DIR, "data", "training_data_v27.csv") PAPER_LOG = os.path.join(AI_DIR, "data", "paper_trades.csv") MS_ODDS = ["odds_ms_h", "odds_ms_d", "odds_ms_a"] MS_PICKS = ["1", "X", "2"] def load_model(): bst = xgb.Booster(); bst.load_model(os.path.join(MODEL_DIR, "model.json")) with open(os.path.join(MODEL_DIR, "feature_cols.json"), encoding="utf-8") as f: feats = json.load(f) with open(os.path.join(MODEL_DIR, "metadata.json"), encoding="utf-8") as f: meta = json.load(f) return bst, feats, meta def pick_for_rows(df, bst, feats, lo, hi, margin): X = df.reindex(columns=feats).apply(pd.to_numeric, errors="coerce").fillna(0.0).values P = bst.predict(xgb.DMatrix(X)) # [n,3] home/draw/away O = df[MS_ODDS].apply(pd.to_numeric, errors="coerce").fillna(0.0).values implied = np.where(O > 1.0, 1.0/O, np.nan) edge = np.where(np.isnan(implied), -9.0, P - implied) out = [] for i in range(len(df)): k = int(np.argmax(edge[i])); o = float(O[i, k]); e = float(edge[i, k]) staked = (e > margin) and (lo <= o < hi) out.append({"idx": i, "pick": MS_PICKS[k], "odds": round(o, 2), "model_prob": round(float(P[i, k]), 4), "edge": round(e, 4), "staked": staked}) return out def settle(): if not os.path.exists(PAPER_LOG): print("No paper_trades.csv yet."); return pt = pd.read_csv(PAPER_LOG) open_bets = pt[pt["result"].isna()] if "result" in pt.columns else pt if open_bets.empty: print("No open bets to settle."); # settle from training CSV scores if present, else needs DB (left as note) src = pd.read_csv(TRAIN_CSV, low_memory=False, usecols=["match_id","score_home","score_away"]) sc = src.set_index("match_id") def res(row): if not pd.isna(row.get("result")): return row["result"] m = sc.index == row["match_id"] if not m.any(): return np.nan r = sc[m].iloc[0]; sh, sa = r["score_home"], r["score_away"] if pd.isna(sh): return np.nan outcome = "1" if sh > sa else ("X" if sh == sa else "2") won = (str(row["pick"]) == outcome) return "WON" if won else "LOST" pt["result"] = pt.apply(res, axis=1) pt["pnl"] = pt.apply(lambda r: (r["odds"]-1.0) if r["result"]=="WON" else (-1.0 if r["result"]=="LOST" else np.nan), axis=1) pt.to_csv(PAPER_LOG, index=False) s = pt.dropna(subset=["pnl"]) if len(s): print(f"Settled {len(s)} bets: hit={100*(s['result']=='WON').mean():.1f}% " f"ROI={100*s['pnl'].sum()/len(s):+.2f}% net={s['pnl'].sum():+.1f}u") return def main(): ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--features", help="CSV of upcoming matches in training schema") ap.add_argument("--demo", action="store_true", help="use tail of training CSV as 'today'") ap.add_argument("--n", type=int, default=20) ap.add_argument("--lo", type=float, default=1.5) ap.add_argument("--hi", type=float, default=2.2) ap.add_argument("--margin", type=float, default=0.03) ap.add_argument("--settle", action="store_true") ap.add_argument("--log", action="store_true", help="append staked picks to paper_trades.csv") args = ap.parse_args() if args.settle: settle(); return bst, feats, meta = load_model() print(f"Model {meta['version']} (trained {meta['trained_at']}, holdout " f"ROI {meta['holdout_eval']['roi_pct']}%) band[{args.lo},{args.hi}] margin {args.margin}\n") if args.features: df = pd.read_csv(args.features, low_memory=False) demo = False else: df = pd.read_csv(TRAIN_CSV, low_memory=False).sort_values("mst_utc").tail(args.n).reset_index(drop=True) demo = True print("(DEMO: last matches of training CSV as stand-in for today)\n") picks = pick_for_rows(df, bst, feats, args.lo, args.hi, args.margin) staked = [p for p in picks if p["staked"]] print(f"{len(df)} matches scanned -> {len(staked)} STAKED MS picks\n") print(f" {'match_id':<28}{'pick':>5}{'odds':>7}{'model%':>8}{'edge%':>7}" + (" result" if demo else "")) print(" "+"-"*60) log_rows = [] for p in picks: if not p["staked"]: continue r = df.iloc[p["idx"]]; mid = str(r["match_id"]) res = "" if demo: sh, sa = r.get("score_home"), r.get("score_away") if pd.notna(sh): out = "1" if sh>sa else ("X" if sh==sa else "2") res = " WON" if p["pick"]==out else " lost" print(f" {mid:<28}{p['pick']:>5}{p['odds']:>7.2f}{100*p['model_prob']:>8.1f}{100*p['edge']:>+7.1f}{res}") log_rows.append({"logged_at": datetime.datetime.now().isoformat(timespec="seconds"), "match_id": mid, "market": "MS", "pick": p["pick"], "odds": p["odds"], "model_prob": p["model_prob"], "edge": p["edge"], "stake": 1.0, "result": np.nan, "pnl": np.nan}) if args.log and log_rows and not demo: new = pd.DataFrame(log_rows) if os.path.exists(PAPER_LOG): new = pd.concat([pd.read_csv(PAPER_LOG), new], ignore_index=True) new.to_csv(PAPER_LOG, index=False) print(f"\n logged {len(log_rows)} picks -> {PAPER_LOG}") elif args.log and demo: print("\n (--log ignored in --demo; only real upcoming picks are logged)") print("\nReminder: paper-trade only. Stake real money after weeks of forward") print("CLV>0 + ROI>0 (settle with --settle, check scoreboard/clv_report).") if __name__ == "__main__": main()