# Hit-rate stats per variant vs (i) random same-minute entries (binomial vs matched base rate + 2000x random same-size draws) and (ii) plain cross (Fisher). BH within side x split. import numpy as np,pandas as pd,sys,os from scipy.stats import binomtest,fisher_exact def multipletests(p,method='fdr_bh'): p=np.asarray(p,float); n=len(p); o=np.argsort(p); q=p[o]*n/np.arange(1,n+1); q=np.minimum.accumulate(q[::-1])[::-1]; out=np.empty(n); out[o]=np.minimum(q,1); return None,out sys.path.insert(0,os.path.dirname(__file__)); from cwlib import * E=pd.read_pickle(f'{C}/results/events.pkl'); B=pd.read_pickle(f'{C}/results/base_outcomes.pkl').reset_index() SRC=pd.to_datetime(['2026-10-23','2026-10-26']) def split_mask(dates,sp): if sp=='train': return dates<'2026-01-01' if sp=='test': return dates>='2026-01-01' return (dates>='2026-01-01')&~dates.isin(SRC) YS=[10,25,50] def variants(E): V={} for side in ('call','put'): sg=1 if side=='call' else -1 for wd in ('dyn','fixed'): g=E[(E.side==side)&(E.wall==wd)] V[(side,wd,'a_plain')]=(g.index,0) for X in (1,2,3): b=g[g[f's_X{X}']>=0]; V[(side,wd,f'b_X{X}')]=(b.index,X) for Y in YS: cc=b[b.opp_oi_chg30>=Y]; V[(side,wd,f'c_X{X}_Y{Y}')]=(cc.index,X) dd=cc[cc[f'basis_chg5_X{X}']*sg>=3]; V[(side,wd,f'd_X{X}_Y{Y}_B3')]=(dd.index,X) return V if __name__=='__main__': V=variants(E); rng=np.random.default_rng(7); rows=[] Bi=B.set_index(['sgn','s']) for sp in ('train','test','test_ex_src'): Bs=B[split_mask(B.date,sp)]; grp={k:g for k,g in Bs.groupby(['sgn','s'])} for (side,wd,vn),(idx,X) in V.items(): sg=1 if side=='call' else -1; g=E.loc[idx]; g=g[split_mask(g.date,sp)] scol='i' if X==0 else f's_X{X}' plain=E[(E.side==side)&(E.wall==wd)]; plain=plain[split_mask(plain.date,sp)] for ok in OUTK: y=g[f'{ok}_X{X}'].dropna(); n=len(y); h=int(y.sum()) r=dict(split=sp,side=side,wall=wd,variant=vn,outcome=ok,n=n,days=g.date.nunique(),hits=h,rate=h/n*100 if n else np.nan) if n: pools=[grp[(sg,s)] for s,dd in zip(g.loc[y.index,scol],g.loc[y.index,'date'])] pools=[(p[p.date!=dd][ok].values) for p,dd in zip(pools,g.loc[y.index,'date'])] p0=np.mean([p.mean() for p in pools]); r['rand_rate']=p0*100 r['p_binom']=binomtest(h,n,min(max(p0,1e-9),1-1e-9)).pvalue sims=np.sum([p[rng.integers(0,len(p),2000)] for p in pools],axis=0) r['p_rand_emp']=(1+(sims>=h).sum())/2001 if h/n>=p0 else (1+(sims<=h).sum())/2001 py=plain[f'{ok}_X0'].dropna(); r['plain_rate']=py.mean()*100 if vn!='a_plain': r['p_fisher_vs_plain']=fisher_exact([[h,n-h],[int(py.sum()),len(py)-int(py.sum())]])[1] rows.append(r) # excursions S=pd.DataFrame(rows) S['q_bh']=np.nan for (sp,side),g in S.groupby(['split','side']): m=g.p_binom.notna(); S.loc[g.index[m],'q_bh']=multipletests(g.p_binom[m],method='fdr_bh')[1] S['q_bh_fisher']=np.nan for (sp,side),g in S.groupby(['split','side']): m=g.p_fisher_vs_plain.notna(); S.loc[g.index[m],'q_bh_fisher']=multipletests(g.p_fisher_vs_plain[m],method='fdr_bh')[1] S.to_csv(f'{C}/results/hit_stats.csv',index=False) # MFE/MAE summary X=[] for (side,wd,vn),(idx,Xx) in V.items(): g=E.loc[idx] for sp in ('train','test'): h=g[split_mask(g.date,sp)] X.append(dict(side=side,wall=wd,variant=vn,split=sp,n=len(h),mfe60_med=h[f'mfe60_X{Xx}'].median(),mae60_med=h[f'mae60_X{Xx}'].median(),mfe60_mean=h[f'mfe60_X{Xx}'].mean(),mae60_mean=h[f'mae60_X{Xx}'].mean())) pd.DataFrame(X).round(1).to_csv(f'{C}/results/excursions.csv',index=False) pd.set_option('display.width',250) k=S[S.outcome.isin(['p100_s30_w60','p50_s30_w30','p150_s50_w60'])] print(k[k.split!='test_ex_src'].round(3).to_string())