import pandas as pd,numpy as np,matplotlib; matplotlib.use('Agg'); import matplotlib.pyplot as plt R='/workspace/research/flow-forensics-2026-10/recov' M=pd.read_csv(f'{R}/results/freq_by_month.csv') fig,ax=plt.subplots(figsize=(13,4.2)); x=np.arange(len(M)) ax.bar(x,M.big_range,color='#cfd8e3',label='% days with range ≥200 (low→high either order)') ax.bar(x,M.ev200,color='#2a7de1',label='% days with ≥200 recovery from the low (same day, by 15:14)') ax.plot(x,M.v_share_of_big,color='#e8590c',lw=1.6,marker='.',label='share of big-range days that were "low first" (V)') ax.axhline(50,color='#e8590c',ls=':',lw=0.8) ax.set_xticks(x[::3]); ax.set_xticklabels(M.ym[::3],rotation=45,fontsize=8); ax.set_ylabel('%'); ax.legend(fontsize=8,loc='upper left'); ax.set_title('Nifty: same-day ≥200-pt recoveries track the size of the daily range; the V-share hovers near 50%') plt.tight_layout(); plt.savefig(f'{R}/charts/freq_monthly.png',dpi=110); plt.close() T=pd.read_csv(f'{R}/results/today_minutes.csv').set_index('hm').loc['09:15':'11:00'] fig,ax=plt.subplots(4,1,figsize=(12,10),sharex=True) ax[0].plot(T.index,T.nifty,label='Nifty spot'); ax[0].axhline(22600,ls=':',c='gray'); ax[0].axhline(22372,ls=':',c='r'); ax[0].set_title('26-Oct-2026 09:15–11:00 IST (1-min, Upstox)') for t,c,l in [('09:16','#e8590c','trigger 1 (failed)'),('09:26','green','trigger 2 (low)'),('09:52','purple','call covering starts')]: for a in ax: a.axvline(t,c=c,lw=0.8,ls='--') ax[0].text('09:16',22700,'09:16 trigger 1',fontsize=7,color='#e8590c'); ax[0].text('09:26',22680,'09:26 trigger 2',fontsize=7,color='green'); ax[0].text('09:52',22420,'09:52 squeeze',fontsize=7,color='purple') ax[1].plot(T.index,T.pe22400_oi/1e6,label='22400 PE OI (M)'); ax[1].plot(T.index,T.pe22500_oi/1e6,label='22500 PE OI'); ax[1].plot(T.index,T.ce22500_oi/1e6,label='22500 CE OI'); ax[1].plot(T.index,T.ce22600_oi/1e6,label='22600 CE OI'); ax[1].legend(fontsize=7,ncol=4); ax[1].set_ylabel('OI (M units)') ax[2].plot(T.index,T.basis,c='k'); ax[2].set_ylabel('fut − spot (pts)') ax2=ax[3]; ax2.plot(T.index,T.vix,c='r',label='VIX'); ax3=ax2.twinx(); ax3.plot(T.index,T.adv_vs_open,c='g',label='# of 50 stocks above own open'); ax2.set_ylabel('VIX'); ax3.set_ylabel('breadth') ax[3].set_xticks(T.index[::10]); ax[3].tick_params(axis='x',rotation=45) plt.tight_layout(); plt.savefig(f'{R}/charts/today_case.png',dpi=105); plt.close() Q=pd.read_csv(f'{R}/results/rules_oos.csv'); q=Q[Q.split=='test'] fig,ax=plt.subplots(figsize=(10,3.8)); ax.barh(q.rule.str.slice(0,48),q.hit_rate,color=['#2a7de1' if p<0.05 else '#adb5bd' for p in q.p_binom]); ax.axvline(q.base.iloc[0],c='r',ls='--',label=f'base rate {q.base.iloc[0]:.1f}%') for i,(h,n) in enumerate(zip(q.hit_rate,q.n_fire)): ax.text(h+0.5,i,f'{h:.0f}% (n={n})',va='center',fontsize=8) ax.set_xlabel('% of triggers that went on to +200 from the low before breaking it (2026 out-of-sample)'); ax.legend(fontsize=8); plt.tight_layout(); plt.savefig(f'{R}/charts/rules_oos.png',dpi=110); plt.close() print('charts ok')