# Shared engine: spot day arrays, signals for candidates A,B15,B30,C,D,E, option-buyer simulator with exits E0..E4 and cost scenarios. import pandas as pd,numpy as np,glob,os B='/workspace/research/flow-forensics-2026-10'; R=f'{B}/recov' NM=385 # minutes 09:15..15:39 def hm2i(h): return int(h[:2])*60+int(h[3:])-555 def i2hm(i): i+=555; return f'{i//60:02d}:{i%60:02d}' TRAIN_END=pd.Timestamp('2025-12-31'); TEST_START=pd.Timestamp('2026-01-01'); LAST=pd.Timestamp('2026-10-23') def split(d): return 'train' if d<=TRAIN_END else 'test' def lot(t): return 25 if t=pd.Timestamp('2026-04-01')) else 0.001)*sv txn=0.0003503*(bv+sv); sebi=1e-6*(bv+sv); stamp=3e-5*bv; gst=0.18*(brk+txn+sebi) return (brk+stt+txn+sebi+stamp+gst)/Q def load_spot(): P=pd.read_parquet(f'{B}/lows/data/minute_panel_v.parquet',columns=['date','hm','open','high','low','close','fvol']) P=P[P.date<=LAST]; S={} for d,g in P.groupby('date'): a=np.full((5,NM),np.nan); ix=g.hm.map(hm2i).values; ok=(ix>=0)&(ix0,num/den,np.nan) def clipstop(entry,stop,d): Rr=abs(entry-stop); Rr=min(max(Rr,15.0),70.0); return entry-d*Rr,Rr # ---------------- signals ---------------- def signals(S,PC,ruleB=None): out=[]; days=sorted(S); FHR={} for k,d in enumerate(days): a=S[d]; o,h,l,c,v=a if np.isnan(o[0]) or np.isnan(c[29]) or np.isnan(c[59]): continue vw=vwap(a) fh=np.nanmax(h[:60])-np.nanmin(l[:60]); FHR[d]=fh # A: O30 mv=c[29]-o[0] if abs(mv)>150: dd=1 if mv>0 else -1; st=(np.nanmin(l[15:30])-5) if dd>0 else (np.nanmax(h[15:30])+5) out.append(dict(cand='A',date=d,m=29,dir=dd,entry=c[29],stop=st)) # B15/B30 gap=o[0]-PC.get(d,np.nan) if gap==gap and gap!=0 and abs(gap)/PC[d]<=0.015: for nm,N in (('B15',15),('B30',30)): orh=np.nanmax(h[:N]); orl=np.nanmin(l[:N]); mid=(orh+orl)/2 for i in range(N,106): if np.isnan(c[i]): continue if c[i]>orh or c[i]orh else -1 if dd==np.sign(gap): out.append(dict(cand=nm,date=d,m=i,dir=dd,entry=c[i],stop=mid)) break # D: VWAP pullback m0=c[29]-o[0]; dd=1 if (m0>30 and c[29]>vw[29]) else (-1 if (m0<-30 and c[29]0 and l[i]<=vw[i]+5) or (dd<0 and h[i]>=vw[i]-5): touched=i if touched is None: continue if (dd>0 and c[i]vw[i]+15): break if i>touched or (i==touched): if i>0 and ((dd>0 and c[i]>h[i-1] and c[i]>vw[i]) or (dd<0 and c[i]touched: st=(np.nanmin(l[touched:i+1])-5) if dd>0 else (np.nanmax(h[touched:i+1])+5) out.append(dict(cand='D',date=d,m=i,dir=dd,entry=c[i],stop=st)); break # E: trend day (median of prior 20 sessions' FHR, prior days only) prior=[FHR[x] for x in days[max(0,k-25):k] if x in FHR][-20:] if len(prior)>=15: med=np.median(prior); hi=np.nanmax(h[:60]); lo=np.nanmin(l[:60]); pos=(c[59]-lo)/fh if fh>0 else np.nan if fh>=1.2*med and abs(c[59]-vw[59])>=0.25*fh: if c[59]>vw[59] and pos>=0.75: out.append(dict(cand='E',date=d,m=59,dir=1,entry=c[59],stop=vw[59])) if c[59]=14.46)&(C.date<=LAST)] # ---------------- spot exit logic (returns exit minute index of decision bar, reason) ---------------- def spot_exit(a,m,dd,entry,stop,Rr,ex): o,h,l,c,v=a; st=stop; best=entry; be=False tgt={'E2':2,'E3':4}.get(ex) for x in range(m+1,360): if np.isnan(c[x]): continue if (dd>0 and l[x]<=st) or (dd<0 and h[x]>=st): return x,'stop' if tgt and ((dd>0 and h[x]>=entry+tgt*Rr) or (dd<0 and l[x]<=entry-tgt*Rr)): return x,'target' if ex=='E4': best=max(best,c[x]) if dd>0 else min(best,c[x]) if dd*(best-entry)>=Rr: ns=best-dd*Rr; st=max(st,ns,entry) if dd>0 else min(st,ns,entry) return 359,'time' def spot_proxy(a,m,dd,entry,stop,Rr,ex): if ex=='E0': return np.nan x,why=spot_exit(a,m,dd,entry,stop,Rr,ex); o,h,l,c,v=a px=o[x+1] if x+1<360 and not np.isnan(o[x+1]) else c[x] return dd*(px-o[m+1]) if not np.isnan(o[m+1]) else np.nan # ---------------- options ---------------- _OPT=None; _EXPS=None def opt(): global _OPT,_EXPS if _OPT is None: _OPT=pd.read_pickle(f'{R}/data/opt_panels.pkl'); _EXPS=sorted({e for _,e in _OPT}) return _OPT def nexp(d,skip0=True): opt(); c=[e for e in _EXPS if e>=d and not(skip0 and e==d) and (d,e) in _OPT]; return c[0] if c else None _cache={} def oarr(d,e,typ,K): key=(d,e,typ,K) if key in _cache: return _cache[key] O=opt().get((d,e)); r=None if O is not None and ('close',typ,K) in O.columns: r=np.full((4,NM),np.nan); ix=np.array([hm2i(x) for x in O.index]); ok=(ix>=0)&(ix200000: _cache.clear() _cache[key]=r; return r def next_px(oa,x,limit=370): """first real option OPEN at minute>=x (<=15:25); never filled.""" for y in range(x,min(limit,NM)+1): if y0 else 'PE' K=float(round(entry/50)*50)+(-dd*50*strike_off) # strike_off=+1 -> 1 ITM, -1 -> 1 OTM oa=oarr(d,e,typ,K) if oa is None or m+1>=NM or np.isnan(oa[0,m+1]): return dict(status='no_option_bar') cs=COSTS[cost]; S=cs['slip']; raw=oa[0,m+1]; fill=raw+S; sells=[]; why='' if ex=='E0': rem=1.0; stopp=fill-20; peak=fill; t1=t2=False; end=min(m+1+60,370); last=None for x in range(m+1,end+1): if np.isnan(oa[0,x]): continue last=x; op,hi,lo,cl=oa[:,x] if lo<=stopp: px=min(op,stopp) if x>m+1 else stopp; sells.append((rem,px-S)); rem=0; why='prem_stop'; break if not t1 and hi>=fill+20: sells.append((0.4,fill+20-S)); rem-=0.4; t1=True; stopp=max(stopp,fill) if not t2 and hi>=fill+40: sells.append((0.3,fill+40-S)); rem-=0.3; t2=True peak=max(peak,hi) if t2: stopp=max(stopp,peak-20,fill+20) if rem>1e-9: if last is None: return dict(status='no_option_bar') sells.append((rem,oa[3,last]-S)); why=why or 'cap60' xexit=last else: x,why=spot_exit(a,m,dd,entry,stop,Rr,ex) px,y=next_px(oa,x+1) if y is None: v=np.where(~np.isnan(oa[3,:371]))[0]; v=v[v>m] if not len(v): return dict(status='no_option_bar') px=oa[3,v[-1]]; y=v[-1]; why+='_lastclose' sells=[(1.0,px-S)]; xexit=y g=sum(f*p for f,p in sells)-fill; c=charges(d,fill,sells,cs['stt_new']) return dict(status='ok',expiry=e,dte=(e-d).days,K=K,typ=typ,fill=fill,gross=g,cost=c,pnl=g-c,pnl_rs=(g-c)*lot(d),exit_min=i2hm(xexit) if xexit is not None else '',why=why,n_orders=1+len(sells))