Files
iddaai-be/ai-engine/scripts/generate_daily_picks.py
T
fahricansecer 9e41407cb5
Deploy Iddaai Backend / build-and-deploy (push) Successful in 35s
gg3
2026-06-05 00:36:24 +03:00

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()