# Step 1: same-day recovery events from intraday low (spot 09:15-15:14 only; 15:15+ treated missing for all years) import pandas as pd,numpy as np B='/workspace/research/flow-forensics-2026-10' P=pd.read_parquet(f'{B}/lows/data/minute_panel_v.parquet',columns=['timestamp','open','high','low','close','date','hm']) t=pd.read_csv(f'{B}/recov/data/today/NIFTY.csv'); t['timestamp']=pd.to_datetime(t.ts.str[:19]); t['date']=t.timestamp.dt.normalize(); t['hm']=t.timestamp.dt.strftime('%H:%M') P=pd.concat([P[P.date<'2026-10-26'],t[['timestamp','open','high','low','close','date','hm']]],ignore_index=True) P=P[(P.hm<='15:14')&P.close.notna()] D=pd.read_parquet('/workspace/research/common/data/nifty_daily_hist.parquet')[['date','close']] u=pd.read_csv(f'{B}/data/1m/NIFTY_day.csv'); u['date']=pd.to_datetime(u.ts.str[:10]); D=pd.concat([D,u[['date','close']]]).drop_duplicates('date',keep='last').sort_values('date') D=pd.concat([D,pd.DataFrame({'date':[pd.Timestamp('2026-10-26')],'close':[np.nan]})],ignore_index=True) D['pc']=D.close.shift(1); pc=D.set_index('date').pc rows=[] for d,g in P.groupby('date'): g=g.reset_index(drop=True) if len(g)<300 and d!=pd.Timestamp('2026-10-26'): continue i=g.low.idxmin(); L=g.low[i]; tl=g.hm[i] hb=g.high[:i+1].max(); after=g.high[i:]; R=after.max(); tr=g.hm[i+after.values.argmax()] # minutes from low to first +200 / +150 / +250 def tt(x): k=np.where(after.values>=L+x)[0]; return int(k[0]) if len(k) else np.nan p=pc.get(d,np.nan) rows.append(dict(date=d,n=len(g),open=g.open[0],prev_close=p,gap=g.open[0]-p,low=L,t_low=tl,fall_pc=p-L,fall_hi=hb-L,fall_open=g.open[0]-L, rec=R-L,t_rechigh=tr,last=g.close.iloc[-1],close_vs_low=g.close.iloc[-1]-L,m200=tt(200),m150=tt(150),m250=tt(250), rng=g.high.max()-L,rec_pct=(R-L)/L*100,fall_hi_pct=(hb-L)/L*100)) E=pd.DataFrame(rows) for x in (150,200,250): E[f'ev{x}']=E.rec>=x E['big_range']=E.rng>=200 E['low_first']=E.ev200 # with range>=200, ev200 means low came first and >=200 recovered E['ev_pct']=E.rec_pct>=0.9 # ~200 pts at 22,500 E['sharp_fall']=(E.fall_hi>=150)|(E.fall_pc>=150) E['year']=E.date.dt.year; E['ym']=E.date.dt.strftime('%Y-%m') E.to_csv(f'{B}/recov/results/day_events.csv',index=False) agg=lambda g:pd.Series(dict(days=len(g),ev150=g.ev150.mean()*100,ev200=g.ev200.mean()*100,ev250=g.ev250.mean()*100,ev_0p9pct=g.ev_pct.mean()*100, sharp_fall=g.sharp_fall.mean()*100,ev200_given_fall=g[g.sharp_fall].ev200.mean()*100,med_range=g.rng.median(),big_range=g.big_range.mean()*100,v_share_of_big=(g.ev200.sum()/max(g.big_range.sum(),1))*100,med_rec=g.rec.median())) Y=E.groupby('year').apply(agg).round(1); print(Y.to_string()); Y.to_csv(f'{B}/recov/results/freq_by_year.csv') M=E.groupby('ym').apply(agg).round(1); M.to_csv(f'{B}/recov/results/freq_by_month.csv'); print(M.tail(15).to_string()) r=E[E.date>='2026-08-01']; print('\nAug-Oct 2026 events200:'); print(r[r.ev200][['date','open','prev_close','low','t_low','fall_pc','fall_hi','rec','t_rechigh','m200']].to_string()) print(E.tail(3).T)