The ₹9 crore spreadsheet
At one point a side strategy of mine backtested at 76.9% CAGR. Compounded forward, the spreadsheet promised ₹9 crore. When a backtest says you're about to be rich, it isn't a forecast. It's a bug report.
The first bug was the universe. I'd been testing on today's Nifty 500 projected backwards, a list that only contains companies that survived. I guessed that was worth 12.7pp a year. Wrong again: measured properly it's +3.8pp a year, handed out free to anyone, including someone picking at random.
Beat a random portfolio, not zero
So the benchmark changed. Every idea now has to beat five random rankers drawing from the same pool, on the same dates, through the same production code, paired date by date. Five monkeys with dartboards, except the monkeys pay the same costs and taxes I do.
The monkeys went four for four:
- The ranker from my August write-upIts "top picks" were a 19-way tie at a capped score, broken by a feature with no signal.coin flip
- Its fixA continuous score instead of a checklist.+0.44pp · t = +1.6
- The next twoMeasured on a universe rebuilt as of every date.−0.02pp · −0.08pp
All four retired before they touched real money. That is the control doing its job.
Six ways the data lied
NSE's raw bhavcopy is a daily file of what printed, not a research dataset. Each of these was caught by a check built for exactly that job, then fixed for good. Some favourites:
Ask NSE for a holiday's file and it answers HTTP 200 with the previous day's data. 80 holidays had been saved as perfect copies, 159,855 rows, so 2020, 2021 and 2024 each had 261 "sessions": every weekday. Every holding period ran ~5% short.
trust the file's own date field, never the URL
A blue-chip bank showed up in my live email 81.5% below its 52-week high. The real figure was 7.5%. It had split, and NSE prices are raw.
923 corporate actions detected, adjusted on read
A gold ETF's 1:100 split had gone unnoticed since 2019. Its sibling bug, reverse splits, had one stock reading +3,118%.
bigger split factors, detection in both directions
A money-market fund topped my stock ranking with a score 2.5x the next name. Funds trade in the same series as shares; only the ISIN prefix tells them apart.
INE in, INF out, at the universe layer
The simulator quietly dropped any stock that delisted mid-trade. Delistings follow collapses, so it was deleting bad outcomes only.
137 delistings priced, stress-tested as total losses
Re-running a past date read prices the scheduler had stored since. Same date, two runs, two different portfolios.
every read takes an as-of date
Then I audited the "honest" version
By mid-September the cleaned-up backtest said 29.6% CAGR. Before publishing it, I did what the system is built for and audited it, as developer, market analyst and researcher at once. It found 14 defects and took the number to 19.1%.
The scariest one wasn't in the data at all. A date join plus a pandas 3 change (pct_change stopped padding gaps by default) meant production's signal correlated only 0.92 with the one I'd validated. On the same panel they scored 23.5% vs 30.1%. The backtest had been grading a strategy I wasn't running.
Here's the part I'm proud of: the edge barely flinched. Through all 14 fixes, the per-pick edge over random went from +4.83pp to +4.39pp, still at t = +11.3. The bugs had inflated the compounding, not the signal.
A two-day change worth +4.9pp. Allegedly.
CAGR, at t = +6.8.
Pure path luck.
Different start dates share most of their trades, so luck survives averaging and t-stats. Two of my September "wins" turned out to live inside this noise floor. Now parameters are chosen on a path-free per-pick measure, and CAGR is quoted as a mean over 15 economically equivalent configs × 45 start dates = 675 paths. By that estimator the old book was 21.8% (its single config had landed in an unlucky pocket). Re-shaping the book on the per-pick measure took it to 24.6%, ahead of the old book in 15 of 15 equivalent configs.
+4.77pp per pick over random. t = +13.0.
Positive in both halves of the sample, and simulated the boring, honest way: point-in-time universe, production code path, next-day-open fills, gap-downs filled at the open, 0.5% round trip, and STCG/LTCG plus cess netted per financial year.
Pre-tax it's 30.9%. Even the 10th-percentile path beats the index by 5 points, and the unluckiest of 675 paths still made money (+5.4% a year). The price is the ride: median max drawdown −30%, worst −54%. Blending in an ETF basket keeps the return (~23.4%) and pulls the worst drawdown in to −29%.
I'd once claimed the stock book and the ETF basket were uncorrelated, at −0.096. Measured honestly: +0.59. Another lie, caught.
The system that caught all of it
A backtest that lies is normal. One that gets caught, every time, before money moves, is the actual product. Random controls, noise twins, a path-noise null and as-of reads exposed every lie above, and they now gate every new idea. Here's what else held up:
Every bug on this page was caught in research, before a single live trade.
+4.83pp per pick before the audit, +4.39pp (t = +11.3) after all 14 fixes, +4.77pp (t = +13.0) after re-shaping. Highly significant at every step.
Production and research share one universe function (500/500 parity) and one signal implementation. A missing signal column now raises instead of silently falling back.
Every trading evening, unattended: launchd, a pmset wake, caffeinate, per-step wall clocks (added after a Gemini call hung for 27 hours) and re-runs that are safe to fire twice.
3.53M rows in SQLite. Swapping 1.37M DataFrame.iloc calls for NumPy made the research loop 8.5x faster, bit-identical across ~92,000 simulated trades.
Every allocator bug reproduced by a failing test before the fix, like one that proposed ₹29,797 of orders against ₹5,910 of cash.
Most ideas die. That's the job.
250+ specifications tested; roughly 1 in 70 survived. That isn't a bad hit rate, it's the published one: Hou, Xue and Zhang found 64% of 447 published anomalies insignificant. Every result, pass or fail, is logged with its number, so no idea gets paid for twice.
This project has shipped more value by removing things than by adding them. The enemy isn't ignorance. It's plausibility.