# "Call-writer squeeze" detector (hypothesis from 23-Oct who-flow study + 26-Oct): after a sharp fall and a base above the low, # price breaks to a new post-low high while call OI near the money drops and futures basis widens. Compared with the same price-only breakout. import pandas as pd,numpy as np B='/workspace/research/flow-forensics-2026-10'; R=f'{B}/recov' OPT=pd.read_pickle(f'{R}/data/opt_panels.pkl') P=pd.read_parquet(f'{B}/lows/data/minute_panel_v.parquet',columns=['open','high','low','close','fut','foi_front','date','hm']); P=P[P.date>='2024-10-21'] def rd(f): d=pd.read_csv(f); d['date']=pd.to_datetime(d.ts.str[:10]); d['hm']=d.ts.str[11:16]; return d tn=rd(f'{R}/data/today/NIFTY.csv'); tf=rd(f'{R}/data/today/NIFTYFUT.csv').set_index('hm'); tn['fut']=tn.hm.map(tf.close); tn['foi_front']=tn.hm.map(tf.oi) P=pd.concat([P,tn[P.columns]],ignore_index=True) D=pd.read_csv(f'{R}/results/day_events.csv',parse_dates=['date']).set_index('date') exps=sorted({e for _,e in OPT}) def nexp(d): c=[e for e in exps if e>=d]; return c[0] if c else None rows=[] for d,g in P.groupby('date'): g=g[g.hm<='15:14'].set_index('hm').sort_index() if len(g)<100 or g.close.isna().all(): continue pc=D.prev_close.get(d,np.nan); O=OPT.get((d,nexp(d))) H=list(g.index); hi=g.high.values; lo=g.low.values; cl=g.close.values; f=g.fut.values; foi=g.foi_front.values if O is not None: ce=O['oi']['CE'] if 'CE' in O['oi'].columns.get_level_values(0) else None RL=np.inf; tRL=0; done_for=None; n=0 for i,h in enumerate(H): if np.isnan(lo[i]): continue if lo[i]'14:14' or i<15: continue hb=max(pc if pc==pc else -np.inf,np.nanmax(hi[:tRL+1])); fall=hb-RL if fall<120 or i-tRL<5: continue post=np.nanmax(cl[tRL:i]) if not(cl[i]>post and 40<=cl[i]-RL<=150): continue # breakout to a new post-low closing high, >=40 above low r=dict(date=d,hm=h,t_low=H[tRL],RL=RL,entry=cl[i],fall=fall,base_min=i-tRL,run_now=cl[i]-RL, basis_chg5=(f[i]-cl[i])-(f[i-5]-cl[i-5]),futoi_chg5_pct=(foi[i]/foi[i-5]-1)*100 if foi[i-5]>0 else np.nan) if O is not None and ce is not None: K=round(cl[i]/50)*50; ks=[k for k in ce.columns if K<=k<=K+200] a=ce.loc[:,ks].reindex([H[i-5]]).sum(axis=1,min_count=1).iloc[0]; b=ce.loc[:,ks].reindex([h]).sum(axis=1,min_count=1).iloc[0] r['call_oi_chg5_pct']=(b/a-1)*100 if a==a and a>0 else np.nan pe=O['oi']['PE']; kp=[k for k in pe.columns if K-200<=k<=K] a=pe.loc[:,kp].reindex([H[i-5]]).sum(axis=1,min_count=1).iloc[0]; b=pe.loc[:,kp].reindex([h]).sum(axis=1,min_count=1).iloc[0] r['put_oi_chg5_pct']=(b/a-1)*100 if a==a and a>0 else np.nan fl=lo[i+1:]; fh=hi[i+1:]; brk=np.where(fl=RL+200,y_entry100=mx>=cl[i]+100, mae60=cl[i]-np.nanmin(np.r_[lo[i],fl[:60]]), mfe60=np.nanmax(np.r_[hi[i],fh[:60]])-cl[i]) r['episode']=f'{d:%Y-%m-%d}_{RL}'; rows.append(r) S=pd.DataFrame(rows); S.to_csv(f'{R}/results/squeeze_candidates.csv',index=False) print(len(S),'breakout candidates on',S.date.nunique(),'days'); print(S.tail(5).T)