"""Cage-day classifier — pre-reg studies/2026-09-09-cage-classifier-prereg.md.""" import gzip, json, glob, os, numpy as np, pandas as pd, warnings from scipy import stats from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score warnings.filterwarnings("ignore") src=open("crown_policies.py").read().split("\ndf=run()")[0]; cp={}; exec(src, cp) gp={}; exec(open("gamma_policy.py").read().split("\nsnaps={}")[0], gp); compute_gamma=gp["compute_gamma"]; f=gp["f"] of={}; exec(open("open_factors.py").read().split("\nprint(\"loading…\"")[0], of) # sessions(), opex_week() FP=cp["FP"]; d=cp["load_nq"](); QUAD=cp["QUAD"] hist={} for p in sorted(glob.glob(f"{FP}/gex/history/*.json.gz")): j=json.load(gzip.open(p)); hist[pd.Timestamp(j["date"]).date()]=dict(rows=j["qqq"],close=j["qqq_close"]) NQ=of["sessions"]("NQ"); NQ["rng"]=NQ.h-NQ.l; NQ["atr20"]=NQ.rng.shift(1).rolling(20).mean(); NQ["prev_c"]=NQ.c.shift(1) vxn=pd.read_csv(f"{FP}/fred/VXNCLS.csv"); vxn.columns=["d","v"]; vxn["d"]=pd.to_datetime(vxn["d"]).dt.date; vxn["v"]=pd.to_numeric(vxn["v"],errors="coerce"); vxn=vxn.set_index("d")["v"].dropna() vx=vxn.reindex(NQ.index).ffill(); NQ["vxn_shift"]=(vx.shift(1)/vx.shift(2)-1).values sessions=sorted(d["sday"].unique()); prev_of={sessions[i]:sessions[i-1] for i in range(1,len(sessions))} nq0910={s:float(g[g["min"]<=550].iloc[-1]["close"]) for s,g in d.groupby("sday") if len(g[g["min"]<=550])} rows=[]; walls={} for sday,g in d.groupby("sday"): p=prev_of.get(sday); rth=g[(g["min"]>=570)&(g["min"]<960)].reset_index(drop=True) if not p or p not in hist or len(rth)<300 or sday in QUAD or sday not in NQ.index or sday not in nq0910 or p not in NQ.index: continue s0=hist[p]["close"]*nq0910[sday]/NQ.loc[p,"c"]; gm=compute_gamma(hist[p]["rows"],s0) if not gm: continue o=rth.iloc[0]["open"]; k=o/s0; cw=gm["call"]*k; pw=gm["put"]*k; r=NQ.loc[sday] if not np.isfinite(r.atr20) or cw<=pw: continue walls[sday]=(cw,pw,gm["regime"],rth) rows.append(dict(date=sday,cage=float(pw<=rth.iloc[-1]["close"]<=cw),onr=(r.on_h-r.on_l)/r.atr20,tilt=gm["tilt"],long=float(gm["regime"]=="long"),short=float(gm["regime"]=="short"), gap=(o-r.prev_c)/r.atr20,absgap=abs(o-r.prev_c)/r.atr20,vxn_shift=r.vxn_shift,width=(cw-pw)/r.atr20,pos=float(np.clip((o-pw)/(cw-pw),-0.5,1.5)),opex=float(of["opex_week"](sday)))) D=pd.DataFrame(rows).dropna().set_index("date"); print("sessions",len(D),D.index.min(),"→",D.index.max(),"base cage",round(D.cage.mean(),3)) FEATS=["onr","tilt","long","short","gap","absgap","vxn_shift","width","pos","opex"] def wf(feats): out=[] for Y in (2025,2026): yr=pd.Series(D.index).apply(lambda x:x.year).values; tr=D[yr=hi,"top",np.where(pte=0.65 and top.cage.mean()>=0.80 and bot.cage.mean()<=0.55 L=[f"# Cage-day classifier — results · {pd.Timestamp.now():%Y-%m-%d %H:%M}\n",f"Pre-registration: `2026-09-09-cage-classifier-prereg.md`. Sessions {len(D)} ({D.index.min()} → {D.index.max()}), base cage rate {D.cage.mean()*100:.1f}%. OOS = 2025–2026 ({len(O)} sessions).\n", "## Classifier (walk-forward OOS)",f"- Full model AUC **{auc:.3f}**; baseline regime-only {auc_a:.3f}; width-only {auc_b:.3f}.", f"- Top-tercile predicted cage days: cage held **{top.cage.mean()*100:.1f}%** (n={len(top)}); bottom tercile **{bot.cage.mean()*100:.1f}%** (n={len(bot)}); mid {O[O.terc=='mid'].cage.mean()*100:.1f}%.", f"- Standardised coefficients (final fit): "+", ".join(f"{k} {v:+.2f}" for k,v in coef.items())+".", f"→ **{'VALIDATED' if FIT else 'REFUTED'}** (rule: AUC ≥ 0.65, top ≥ 80%, bottom ≤ 55%).\n","## Univariate cage rate by quintile (full sample, descriptive)"] for ft in ["onr","tilt","gap","absgap","vxn_shift","width","pos"]: q=pd.qcut(D[ft].rank(method="first"),5,labels=["Q1","Q2","Q3","Q4","Q5"]); L.append(f"- {ft}: "+" · ".join(f"{k} {v*100:.0f}%" for k,v in D.groupby(q).cage.mean().items())) L.append("- regime: "+" · ".join(f"{k} {v*100:.0f}%" for k,v in D.groupby(np.where(D.long==1,'long',np.where(D.short==1,'short','neutral'))).cage.mean().items())) # ---- conditional trade rules on OOS sessions ---- res=[] for sday,rw in O.iterrows(): cw,pw,regime,rth=walls[sday]; f5=cp["five_min"](rth); c=f5.c.values def fade(i): if i==0: return 0 if c[i-1]>cw and c[i]<=cw: return -1 if c[i-1]=pw: return 1 return 0 brk=lambda i:(1 if c[i]>cw else (-1 if c[i]=mid]; p=stats.ttest_1samp(t.r,0,alternative="greater").pvalue; ok=t.r.mean()>0 and p<0.025 and h1.r.mean()>0 and h2.r.mean()>0 and len(t)>=50 v=("VALIDATED" if ok else "REFUTED") if (FIT and pol.startswith("C")) else "descriptive" L.append(f"- **{pol}**: eligible {g.elig.sum()}, triggers {len(t)}, W/L/T {(t.res=='win').sum()}/{(t.res=='loss').sum()}/{(t.res=='timeout').sum()}, R/trigger **{t.r.mean():+.3f}** (p={p:.3f}), halves {h1.r.mean():+.3f}/{h2.r.mean():+.3f}, $ at 1 NQ {t.pts.sum()*20:+,.0f} → {v}.") out="\n".join(L); open(f"{FP}/studies/2026-09-09-cage-classifier-results.md","w").write(out); print(out)