155 lines
7.1 KiB
Python
155 lines
7.1 KiB
Python
"""
|
|
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()
|