# shared: load compact panel, outcome function, option-trade simulator (costs/slippage as recov/scripts/tradelib.py) import pickle,numpy as np,pandas as pd C='/workspace/research/flow-forensics-2026-10/callwall' HM=[f'{(555+m)//60:02d}:{(555+m)%60:02d}' for m in range(375)] LAST=359 # 15:14 = last usable spot minute (15:15-15:29 frozen since 2026-08-03 -> missing; capped for all days for consistency) LASTX=179 # 14:14 = last cross minute TS=[50,100,150]; SS=[30,50]; WS=[30,60] OUTK=[f'p{t}_s{s}_w{w}' for w in WS for s in SS for t in TS] def load(): return pickle.load(open(f'{C}/data/cw_days.pkl','rb')) def outcome(D,s,sgn): """sgn=+1 long spot (calls), -1 short (puts). entry=spot close at signal minute s. Same-bar target&stop -> stop (conservative).""" c=D['c'];h=D['h'];l=D['l']; e=c[s] if e!=e: return None r={} end=min(s+60,LAST) fav=(h[s+1:end+1]-e) if sgn>0 else (e-l[s+1:end+1]); adv=(e-l[s+1:end+1]) if sgn>0 else (h[s+1:end+1]-e) for w in WS: n=min(s+w,LAST)-s; F=fav[:n]; A=adv[:n] for S in SS: for T in TS: hit=0 for f,a in zip(F,A): if a!=a: continue if a>=S: break if f>=T: hit=1; break r[f'p{T}_s{S}_w{w}']=hit r['mfe60']=np.nanmax(fav) if np.isfinite(fav).any() else np.nan; r['mae60']=np.nanmax(adv) if np.isfinite(adv).any() else np.nan r['nbars60']=int(np.isfinite(fav).sum()) return r SLIP=3.0 def lot(t): return 25 if t0) / PE (sgn<0) strike Kopt, nearest expiry, at minute s+1 open + slip. Spot stop relative to crossed strike Kwall.""" tp='CE' if sgn>0 else 'PE'; ki=np.where(D['K']==Kopt)[0] if len(ki)==0: return dict(status='no_strike') k=ki[0]; O=D['open_'+tp][:,k];Hh=D['high_'+tp][:,k];Ll=D['low_'+tp][:,k];Cc=D['close_'+tp][:,k] if s+1>LAST or O[s+1]!=O[s+1]: return dict(status='no_fill_bar') fill=O[s+1]+SLIP; e=D['c'][s]; p=EXITS[ex]; end=min(s+1+hold,LAST) sh=D['h'];sl=D['l'] stop_sp=(Kwall-p['stop']*sgn) if 'stop' in p else None rem=1.0; sells=[]; pstop=fill-20; t1=t2=False; peak=fill; spk=e; pending=False; why='time' for x in range(s+1,end+1): ok=Cc[x]==Cc[x] hx,lx=sh[x],sl[x] if hx==hx: fav=(hx-e) if sgn>0 else (e-lx); spk=max(spk,hx) if sgn>0 else min(spk,lx) adv_hit=stop_sp is not None and ((lx<=stop_sp) if sgn>0 else (hx>=stop_sp)) if adv_hit: pending=True; why='spot_stop' elif 'tgt' in p and fav>=p['tgt']: pending=True; why='spot_target' if 'trail' in p and not pending: a,b=p['trail'] if abs(spk-e)>=a: ts=spk-b*sgn stop_sp=max(stop_sp,ts) if sgn>0 else min(stop_sp,ts) if pending: if ok: sells.append((rem,Cc[x]-SLIP)); rem=0; break continue if not ok: continue if p.get('prem') or p.get('scale'): if p.get('prem') and Ll[x]<=pstop: px=min(O[x],pstop) if x>s+1 and O[x]==O[x] else pstop; sells.append((rem,px-SLIP)); rem=0; why='prem_stop'; break if not t1 and Hh[x]>=fill+20: sells.append((0.4,fill+20-SLIP)); rem-=0.4; t1=True; pstop=max(pstop,fill) if not t2 and Hh[x]>=fill+40: sells.append((0.3,fill+40-SLIP)); rem-=0.3; t2=True peak=max(peak,Hh[x]) if t2: tr=max(peak-20,fill+20) if Ll[x]<=tr and not p.get('prem'): sells.append((rem,tr-SLIP)); rem=0; why='trail'; break pstop=max(pstop,tr) if rem>1e-9: last=[x for x in range(s+1,end+1) if Cc[x]==Cc[x]] if not last: return dict(status='no_exit_bar') sells.append((rem,Cc[last[-1]]-SLIP)) g=sum(f*q for f,q in sells)-fill; cst=charges(d,fill,sells) return dict(status='ok',fill=fill,gross=g,cost=cst,pnl=g-cst,pnl_rs_lot=(g-cst)*lot(d),exit_reason=why)