import pandas as pd,numpy as np,datetime as dt,json,html L='/workspace/research/popular-strategies'; R=lambda f:pd.read_csv(f'{L}/results/{f}') now=dt.datetime.now(dt.timezone(dt.timedelta(hours=5,minutes=30))).strftime('%Y-%m-%d %H:%M IST') def tb(df,fl=2): return df.to_html(index=False,float_format=lambda x:f'{x:,.{fl}f}',border=0,na_rep='–',escape=True) css=open('/workspace/research/flow-forensics-2026-10/index.html').read().split('')[0] NAMES={'P1':'ORB15 breakout','P2':'ORB5 first 5-min candle','P3':'Prior-day high/low breakout','P4':'Narrow-CPR breakout + VWAP','P5':'Supertrend(10,3) flip + VWAP','P6':'EMA 9/21 cross + VWAP','P7':'RSI(14) 30/70 reversal','P8':'Gap fade (gap-fill)'} RULE={'P1':'First 5m close beyond 09:15–09:29 range (09:30–12:00). Stop = other side of the range.', 'P2':'First 5m close beyond the 09:15–09:19 candle (to 11:00). Stop = other side.', 'P3':'Day opens inside the prior-day range; first 5m close above PDH / below PDL (to 14:00). Stop = breakout bar extreme ±5.', 'P4':'Narrow-CPR day only (width < 30th pct of last 250 days). First 5m close beyond CPR and on the same side of VWAP (09:30–12:00). Stop = other CPR edge.', 'P5':'5m Supertrend(10,3) flips, confirmed by VWAP side (09:45–14:30). Stop = Supertrend line.', 'P6':'5m EMA9 crosses EMA21, confirmed by VWAP side (09:45–14:30). Stop = 6-bar swing ±5.', 'P7':'5m RSI(14) crosses back above 30 (CE) / below 70 (PE) (09:45–14:30). Stop = 6-bar swing ±5.', 'P8':'Gap 0.3–1.5%: first 5m close back through the 15-min range toward the gap fill (to 11:00) → buy against the gap. Stop = other side of range.'} WORKS={'P1':'No stable regime. No filter qualified on the 2024–25 options train. On spot, the walk-forward picked a different label each year (expiry → prior-day range), and the OOS edge was not significant.', 'P2':'Lead: after an NR7 day. Chosen on train (+2.1, n=42). 2026: +28.3 pts (n=29, p=0.025, beats 99% of random subsets), but BH q=0.35. Not proven. On spot, the walk-forward kept picking expiry days, which flipped sign year to year.', 'P3':'Lead: after an NR7 day (2026 +23.0, n=15, p=0.10, q=0.39). On spot, the filter flipped between FII-long and prior-day-range labels. Not proven.', 'P4':'Lead: NR7 / compressed prior day (options 2026 +23.7, n=18; spot 2026 +33, n=28, p=0.009, q=0.30). Only one OOS year exists (no VWAP data before Oct-2024). Not proven.', 'P5':'Mid efficiency-ratio days (0.2–0.4) chosen on train. 2026 +11.9 (n=33, p=0.13). Not proven. The meta-switch hurt.', 'P6':'No. The train-chosen filter (compressed prior day) went negative in 2026 (−3.2).', 'P7':'No. The train-chosen filter (compressed prior day) was −8.0 in 2026, worse than always-on. On spot, no filter qualified in 2025–26.', 'P8':'No. No filter qualified on train. The M20 switch was +26 in 2026, but on only 8 trades.'} P=R('primary_results.csv'); tr=P[P.split=='train'].set_index('cand'); te=P[P.split=='test'].set_index('cand') SY=R('spot_by_year.csv').pivot(index='cand',columns='year',values='spot_net'); PQ=R('positive_quarters.csv').set_index('cand') FM=R('regime_family.csv'); WF=R('spot_walkforward.csv'); MS=R('meta_switch.csv'); RB=R('regime_buckets.csv'); CC=json.load(open(f'{L}/results/cuts_count.json')) N7s=R('nr7_breakouts_spot_by_year.csv'); N7o=R('nr7_breakouts_options_by_year.csv'); E1=R('secondary_E1.csv') rows='' for c in NAMES: sy=' / '.join('–' if pd.isna(SY.loc[c].get(y,np.nan)) else f'{SY.loc[c][y]:+.1f}' for y in [2022,2023,2024,2025,2026]) rows+=f"
| Strategy | Train Oct-24–Dec-25 n / net pts per trade | Test 2026 n / net pts | 2026 win% | 2026 ₹ total (1 lot) | Pctile vs random entries | p (2026) | BH q (8) | Spot net pts per trade 2022 / 23 / 24 / 25 / 26 | Positive quarters spot · options | Works in which regime? |
|---|
| Combo | Train n / mean / win | 2026 n / mean / win | 2026 mean of its single strategies | 2026 random mean (pctile) | p | BH q (11) | Jan–Sep 2026 | Oct 2026 | 2026 without best 3 trades | Counts? |
|---|
| Variant | Exit | Train n / mean / win | 2026 n / mean / win | 2026 mean before charges | 2026 random mean (pctile) | p | BH q (8) | Jan–Sep 2026 | Oct 2026 | 2026 without best 3 | Counts? |
|---|
Built {now}. Research only, not advice. Read-only data, no orders. Option buyer only (long = buy ATM CE, short = buy ATM PE, nearest weekly with DTE≥1). REAL costs ≈2 pts per round trip. Everything below was pre-declared in PREREG.md (19:20 IST, before any 2026 number). Regime addendum after Satya's 19:17 IST question: “could it be each strategy works for certain periods”.
Summary table · Works in which regime? · By period · Rules · Method and limits · Combinations · Premium support · Trader discretion · YouTube live calls · Expiry cheap counter-trend · Why 20? · 20 without time value
Percentile vs random: 2026 mean compared with 2000 sets of random-day entries at the same minute, direction and R, with the same exit (50 = no better than random). Spot net = index points minus 1 pt. VWAP-based P4–P6 start Oct-2024 because futures volume is missing before that. Positive quarters: spot 2022–26 · options Q4-2024–Q4-2026.
How it was tested (pre-declared). Every label is known before 09:15. Rule: on the training window, keep the buckets of one label where the strategy averaged above 0 (with at least 10 option trades or 15 spot trades per bucket). Pick the label with the highest training total. Apply it unchanged to the next period. Options: train Oct-2024–Dec-2025 → test 2026. Spot: expanding walk-forward (train 2022..Y → test Y+1; OOS 2023–2026; the requested 2017 start is impossible because no intraday data before 2022 exists). Meta-switch M20: trade only if the strategy's last 20 completed trades summed above 0. BH over the declared family of 32 tests. A regime edge counts only if q<0.10, the OOS filtered mean beats always-on, and it beats ≥90% of random same-size subsets.
Overfitting disclosure. {CC['n_labels']} labels × {CC['n_buckets']} buckets × 8 strategies × 2 datasets, {CC['bucket_results_reported']} bucket cells (all shown in regime_buckets.csv). With this many cuts, plenty of buckets look great in hindsight. In the heatmaps below, many cells are green on train and a largely different set is green in 2026. That is why only out-of-sample results count.
The chosen label changes almost every year for P1, P2, P3, P7 and P8. A regime that has to be re-picked every year is not a stable property of the strategy.
M20 / M40 = last 20 / 40 trades net positive. 8w = last 8 weeks net positive. Only M20 is in the family; M40 and 8w are secondary. Turning a strategy off after losses mostly misses the rebounds: these strategies mean-revert in performance rather than trend.
Spot net pts per trade by year. NR7 beat non-NR7 days in 2022, 2023, 2025 and 2026, and lost in 2024. The 2026 gap (+50 vs −4) is far larger than in any earlier year, so expect it to shrink. To watch going forward: log every NR7-day ORB/PDH breakout from 27-Oct-2026 and re-check after about 40 trades.

Added after Satya's 21:13 IST question: “what about combination of strategies there probability increases right”. Pre-registered at 21:14 IST in PREREG.md before any combo result. Same data, costs, exit, random benchmark and train/test split. A combo trade is the trade of the confirming (later) signal, one per day. Honesty flag: the NR7 combos (N*) reuse a filter whose 2026 result was already seen, so they are not clean out-of-sample.
Common window 21-Oct-2024 to 23-Oct-2026 ({OM['days']} days). Signal days: {', '.join(f"{k} {v}" for k,v in OM['fires'].items())}. Cell = % of days the row strategy fired on which the column strategy fired in the same direction.
{mx('same')}ORB15 and ORB5 fire the same way on 87–89% of each other's days. When P3 (PDH/PDL) or P4 (CPR) fires, ORB15/ORB5 fire the same way on 75–82% of those days. The indicator group (P5 Supertrend, P6 EMA) agrees 70–84%. P8 (gap fade) is the ORB15 trade 90% of the time. When two of them “agree”, it is usually the same price move counted twice, not two independent confirmations. Option P&L on same-direction co-fire days is also highly correlated (P1–P2 0.91; P1–P8 1.00):
“Counts” needs q<0.10, a 2026 mean above its single strategies' 2026 mean, and ≥90th percentile vs random. PAIR_BEST turned out to be P3+P4, so it is the same test as PAIR_P3P4. BH over the 10 distinct tests gives the same answer (min q 0.16). Train pair ranking: all 28 pairs. Only P3+P4 was above 0 on train (+0.7, n=34). Every other pair lost.
Reading it. Requiring 2 or 3 strategies to agree lowered train expectancy compared with the single strategies (K3_30 −11.0 vs −5.9). By the time 3 signals agree, much of the move is already used up and the stop is wider. In 2026 the k-of-n combos were negative for Jan–Sep and positive only in October. P3+P4 (PDH/PDL breakout on a narrow-CPR day) is the only clean combo with a positive train and 2026 (+0.7 → +19.8, Jan–Sep 2026 +14.6, n=23). But it has 25 trades, q=0.13, a median train trade of −13, and depends on a few big days. It goes on the forward-watch list next to NR7.
On spot, k-of-n agreement flips sign from year to year (K2_15: −0.2, −6.2, +7.3, −4.1, +4.0). P3+P4 needs VWAP for P4, so it only starts Oct-2024.
Added after Satya's 21:24 IST question: “what about option premium support strategy”. Pre-registered at 21:26 IST in PREREG.md before any result. Real 1m ATM weekly option bars 21-Oct-2024 to 23-Oct-2026. The chart is the current ATM strike's own 5m premium chart. Costs ≈0.5 pt per side plus statutory charges (STT 0.15% of sell premium from 1-Apr-2026). Train Oct-2024–Dec-2025, test 2026. Random benchmark = same minute, same CE/PE, ATM on random days of the same split, same exit.
| Study (nifty-setups-backtest) | What it was | Out-of-sample result |
|---|---|---|
| atm-daily-sr | Prior-day ATM CE/PE premiums mapped to spot S/R rails (half-scale), bounce/retest entries; index book SL20, 40%@+20, 30%@+40, trail rest | 275 trades, −10.3 pts per trade net (−0.3 gross); old futures-style costs ~10 pts per trade |
| atm-daily-sr-optbuyer | Same levels, booked as option buyer (SL10, 40%@+10, 30%@+20, trail) | 476 trades, −1.5 option pts per trade net, −0.5 gross, 47% win |
| confluence-v1 | Retailer trap + AMD fake-break + ATM premium black-box support bounce, all same direction | 20 trades, +7.2 gross but −4.2 net (cost ~11 pts per trade), p=0.48 |
| confluence-v1-scaleout | Same entry, SL20 / 40%@+20 / 30%@+40 / trail exit | 30 trades, −0.4 gross, −12.2 net |
| trader-filters-r126 | Black box or round-50 + rejection wick (R1), plus R2/R6 filters, option buyer | All 8 parameter sets net negative (≈ −1.5 to −2 pts per trade, n≈1,000–2,000) |
| trader log (hand-logged stream calls) | 11 “Black Box” trades May–Jul 2026 | 10 of 11 marked WIN. These are discretionary, hand-logged calls with no fixed rule and no losers' log, so they cannot be reproduced or tested. |
The confluence idea was the only one with a positive gross result, and that was on 20 trades. Under corrected costs (~2 pts per round trip instead of ~11), +7.2 gross would still be only about +5 net on n=20, which is not testable evidence.
V1 level = prior-day low of the same contract. V2 = most recent confirmed 5m swing low after 09:30. V3 = multiple of 25 (premium 40–400). V1–V3 need a touch within max(2 pts, 3%) approached from above, then the next 5m close above the touch-bar high. V4 = 5m close above the premium's high of the day so far. Entry is at the next 1m option open. First event per day. “Counts” needs q<0.10, ≥90th percentile vs random, and still positive without the best 3 trades. Pre-charge means include the 0.5 pt per side slippage.
Reading it. Every variant and both exits lost money in both periods, and no BH q is below 1.00. Several are worse than random entries at the same minute (V1, and V3 with the trail exit, sit at the 0–2nd percentile). A premium bouncing off its support does not predict the next move any better than chance. The prior-day premium low is the worst level, probably because overnight theta decay pulls the premium down to it mechanically rather than real buyers defending it. Satya's old scale-out exit and the trail-50% exit give the same answer. October 2026 does not rescue it either (Oct 2026: V3 trail +7.4 on 15 trades; the others are negative).
The train-chosen regime (VIX>16, NR7, uptrend, …) lost money in 2026 in all 6 cases where a filter qualified, and was worse than always-on in 5 of those 6 (V1 had no qualifying filter). 0 of 8 count.
Monthly and regime-bucket tables: prem_by_period.csv · prem_regime_buckets.csv · trades prem_trades.csv.
Added after Satya's 21:34 IST question: “why can't you find those discretion rules”. Design pre-registered at 21:37 IST in PREREG.md (TRADER-DISCRETION) before any taken/skipped or outcome number was seen. Real 1m Nifty ATM weekly option bars. Corrected costs (≈0.5 pt per side plus statutory charges, STT 0.15% of sell premium from 1-Apr-2026).
The log is trader-log/trades_timestamped.csv: 225 rows, 06-May to 05-Oct-2026, built from stream screenshots and the fixed-date Excel files with entry premium and start/stop times. 218 rows are usable (one NO_TRADE row and rows with no time or strike removed): 110 Nifty and 108 Sensex. Sensex rows are mapped to the Nifty ATM equivalent at the same minute. Logged result: 169 WIN, 47 LOSS, 2 BE (78% wins).
How well the log matches real option bars (earlier fill-match work): only 22 of 218 (10%) match the real premium at the logged time (tiers A/B/B2/C: exact, within ±5 or ±15 minutes, or within 5% of the premium). 148 (68%) match only on strike and time, because the log has no usable premium. 47 (22%) disagree with the real bars (wrong premium, or the strike did not trade). When the earlier loose premium matcher was tried at shifted placebo times, it matched about as often as at the logged times (Nifty 29% vs 31% placebo). So the logged premiums do not reliably identify a real fill.
Replay win = the earlier fill-match simulation with the logged stop and target on real 1m bars. X_OLD = Nifty ATM option bought at the next 1m open after the logged time, with the SL20 / 40%@+20 / 30%@+40 / trail exit. Covered = an entry of ours (V1–V4 premium-support or P1–P8) fired the same day, same direction, within ±10 minutes.
What this shows. On real bars the trader's calls are ordinary. The overall replay win rate is 27% versus 78% logged, and the Black Box calls go from 14 of 15 logged wins to 1 of 15 in the replay. Some of the gap is how the replay works: it uses the Nifty ATM and the next 1m open, while he may have used a different strike, a better fill or a manual exit. That gap can't be closed, because 68% of rows have no premium to check against. But a 78% → 27% drop is far too big to be explained by fills alone.
Across the 53 days with log entries our rules made 662 candidate entries; 103 (15.6%) line up with one of his entries. Only 30% of his entries are matched by any rule of ours, and 19% by a premium-support rule. Placebo check: shift his entry times by ±30 or ±60 minutes and the match rate is 8.8–13.1%, or 11% if the direction is flipped. So his entries line up with our setups only slightly more often than chance. Most of what he does is not one of these setups.
{tb(dsrc,1)}The taken candidates did no better than the skipped ones. They were better with the old exit (−2.1 vs −5.1) and worse with the trail-50% exit (−6.0 vs −3.0). Both groups lose money. Skipping those candidates did not avoid losers.
Only three features differ after BH. Time of day: taken entries come ~37 minutes earlier on average (SMD −0.41). Prior trades that day: more trades already that day (+0.29). ATM OI change over 15 minutes: OI rising faster (+0.30). Premium drawdown, speed of fall, bounce speed, distance from the day high/low and from round levels, gap, basis, PCR change and expiry day show no difference (|SMD| ≤ 0.14). “Prior trades” is a property of his day, not of the market, so it cannot become a rule.
| Model | Leave-one-day-out AUC (May–Jul) | Holdout AUC (Aug–Oct) |
|---|---|---|
| Depth-2 tree (min leaf 10) | {DM['tree']['lodo_auc']:.2f} | {DM['tree']['holdout_auc']:.2f} |
| L2 logistic, 17 features | {DM['logit']['lodo_auc']:.2f} | {DM['logit']['holdout_auc']:.2f} |
An AUC of 0.5 means the model does no better than a coin flip at telling taken from skipped. The logistic's 0.61 in CV comes almost entirely from time of day and prior trades (coefficients −1.2 and +0.8). It drops to 0.55 on the held-out months. This repeats the early-Oct remine (trader-filter-mine-timed), where the holdout AUC was ≈0.5 and the best broad rule (near the day extreme and a calm 3-minute move) had lift 1.24 with a CI of 0.84–1.61.
Extraction rule (fixed in advance): take the tree leaf with the highest take rate among leaves with ≥10 takes, provided it is at least 1.2× the base rate. Result: {DRULE}. tod is minutes after 09:15, so 36.5 means before about 09:52 IST. On May–Jul this leaf took 34 of 65 candidates (52%) against a base rate of 16%.
Held-out log months (Aug–Oct 2026): the filter fired {DM['holdout_n']} times and he took {DM['holdout_takes']}. Lift {DM['holdout_lift']:.2f} (base {DM['holdout_base']*100:.0f}%), day-block bootstrap 90% CI {DM['lift_ci90'][0]:.2f}–{DM['lift_ci90'][1]:.2f}. Failed: the CI needed to be above 1.
As a strategy on 2024–26 history (May–Jul 2026 excluded; first filtered candidate per day; random = same candidate minute and type on random days, 30 repeats; BH over 4):
{tb(DH,2)}0 of 4 count. Three of the four are negative and no better than random. The 2026 trail-50% row is +4.5 per trade at the 97th percentile vs random, but q=0.82 and −1.6 without its best 3 trades, the same October-heavy pattern as the rest of this page.
Files: disc_trader_entries.csv · disc_candidates.csv · disc_feature_contrast.csv · disc_model.json · disc_filter_fullhist.csv. Scripts: s9_discretion.py, s9b_fullhist.py, s10_disc_model.py (sklearn), s11_disc_fullhist.py.
{YT_HTML} {EC_HTML} {W20_HTML} {NTV_HTML}| # | Strategy | Rule as coded |
|---|---|---|
| {c} | {NAMES[c]} | {html.escape(RULE[c])} |
Sources: stoxgo.in CPR guide, algoram.in “best NIFTY options buying strategy 2026”, tradingzenith.net “7 setups tested”, Ojha CPR playbook. The VWAP-pullback and with-gap ORB variants were already tested on the buyer-method audit.
Files: results · scripts · charts · pre-registration.
""" open(f'{L}/index.html','w').write(H); print('written',len(H))