import pandas as pd RW=f'{L}/results' WS=pd.read_csv(f'{RW}/w20_scan_levels.csv'); WP=pd.read_csv(f'{RW}/w20_scan_special.csv'); WT=pd.read_csv(f'{RW}/w20_touch_levels.csv'); WD_=pd.read_csv(f'{RW}/w20_touch_round_vs_nonround.csv'); WH=pd.read_csv(f'{RW}/w20_holds_summary.csv'); WL=pd.read_csv(f'{RW}/w20_holds_by_level.csv') nsp=int(WP.special.sum()) WSt=WS[['ix','set','L','n','days','x2','x3','z','r_a','r_b','r_c','r_a_train','r_a_test']].rename(columns={'x2':'P(≥2×) %','x3':'P(≥3×) %','z':'settle≈0 %','r_a':'net/premium (a)','r_b':'net/premium (b)','r_c':'net/premium (c)','r_a_train':'(a) train','r_a_test':'(a) test'}) WPt=WP[['ix','set','metric','at15','at20','at20_ci','at25','smooth_fit_at20','special']] WTt=WT[['ix','set','L','round','n','hold5','hold15','hold15_pct','up15']].rename(columns={'hold5':'low≥L−2 next 5m %','hold15':'low≥L−2 next 15m %','hold15_pct':'low≥0.9L next 15m %','up15':'close>L after 15m %'}) pr=WH[(WH.L==20)&(WH.set=='raw')].copy() prt=pr[['ix','N','exit','split','n','mean','median','win','no_top3','random_mean','random_pctile','p','q_bh_primary','counts']] W20_HTML=f"""

Why 20? Is the ~20 premium level itself special on expiry days?

Added after Satya's 20:14 IST (27-Oct) follow-up: “but there is some thing with this 20 i think”. Design pre-registered at 20:20 IST in PREREG.md (WHY-20) before any number was computed. Same data and rules as the expiry section above: expiring contract, real 1m bars, spot 15:15–15:29 missing, official-close settlement, buyer only, next-open fill + slippage, full costs.

Verdict: 20 is not special.

W1. Level scan: first touch of each level from above (every OTM option, every expiry day)



{tb(WSt,2)}

Band ±20% around each level (20 = 16–24). Net/premium = net P&L ÷ fill; −0.29 means you lose on average 29% of the premium paid. Error bars and CIs are a 95% bootstrap over days. Sensex “scaled” = level × (Sensex open / Nifty open) ≈ ×3.3.

Is 20 an outlier vs a smooth curve fitted to the other 9 levels?

{tb(WPt,3)}

W2. Round-number support: does the premium bounce at 20 more than at 17/18/22/23?

{tb(WTt,1)}

Round {{10,20,25,30,50}} minus non-round {{17,18,22,23}}, and 20 minus mean(17,18,22,23) (percentage points, 95% day-bootstrap CI):

{tb(WD_,2)}

The fixed “low ≥ L−2” measure falls steadily with level: 61% at 10 and 33% at 20 (Nifty, 5 min). The round-number edge in that measure therefore comes from level 10, where −2 is a 20% cushion. With a fair tolerance (≥0.9L) every level is at 12–18% (Nifty), with no bump at 20. 20 vs its non-round neighbours: Nifty +1.1 points (CI −1.2..3.5), Sensex −1.5 (−3.0..0.1).

W3. “Holds at 20” variant (zone 18–22, floor 17, base N minutes, buy the break of the base high)

{tb(prt,2)}

A row counts only if the test mean > 0, p < 0.05, q < 0.10, it is ≥ 90th percentile vs random, the train mean > 0, the mean without the top 3 is > 0, and it beats the same construct at 15 and 25. Random = same window and side, option nearest the level, 30 repeats.

Same construct at every level (mean net pts per trade; Nifty and Sensex raw). 20 is the worst Nifty level on train and the best on test:

{tb(WL,2)}

Time of day (Nifty, holds-at-20): most signals come before 11:00. That window loses on train (−10 to −15) and is about zero on test (−4.2 to +3.1). The 11:00–13:00 and 13:00–14:30 rows have 3–25 trades each and swing sign between train and test. See w20_holds_windows.csv. Sensitivity table (all levels, BH within the table): 20 of 114 test rows are positive and none is significant; the significant rows are negative.

Files: w20_scan_levels.csv · w20_scan_special.csv · w20_touch_levels.csv · w20_touch_round_vs_nonround.csv · w20_holds_summary.csv · w20_holds_by_level.csv · event files w20_scan / w20_touch / w20_holds(_random).csv · scripts s18_why20.py, s19_why20_stats.py, ec_lib.py.

"""