The dataset didn't exist
No file anywhere has this. I scraped it across several sources: IPO fundamentals, listing sheets and five years of daily Nifty closes, which cleaned down to 309 usable IPOs.
Case study 02 · Machine learning & markets
How much does an Indian mainboard IPO gain or lose on listing day? The model uses only what is knowable before it lists, grey market premium, day-wise subscription, and the mood of the Nifty 50. But the real finding isn't the model. It's that a random train/test split scores a flattering R² ≈ 0.55 and is quietly lying to you, because the IPO market cooled sharply after 2024.
01 · How it works
In India retail IPO decisions run on grey market premium. GMP is an unofficial number, and applicants read it as a forecast: GMP says +40%, so expect +40%. On out-of-time data that turns out to be half right. GMP correlates 0.52 with the actual listing gain, so it knows the direction, but scored as a prediction it gets R² ≈ 0.00. It is badly wrong about how far. Closing that gap is the whole job of the model.
No file anywhere has this. I scraped it across several sources: IPO fundamentals, listing sheets and five years of daily Nifty closes, which cleaned down to 309 usable IPOs.
Grey market premium over three days, day-wise subscription, and how the Nifty is behaving. The same IPO lists very differently into a rally than into a correction, so market mood is a feature, not noise.
Meaning the model is only ever trained on IPOs that listed before the one it is predicting, which is how it would work live. Scored this way it gets R² 0.50, and that is the number the app reports.
Gradient boosting plus random forest, both kept deliberately simple. 309 rows is a small dataset and complex models memorise small datasets instead of learning from them. Averaging the two was steadier across folds than either alone.
Scrape, engineer features, validate, train, deploy. Five notebooks run in order, then one artifact ships to the API.
gmp_trend sub_momentum and qib_vs_retail are derived. Date matching is the step everything depends on a wrong join here silently leaks future market data into a feature./health returns the model timestamp, so the health check verifies a loaded model rather than a live process. /api/meta feeds the form. /api/predict validates with Pydantic, builds a one-row frame in the exact trained feature order, averages both estimators, and returns the gain with its error bar.02 · Challenges
I did what you normally do: shuffle the 309 IPOs, hold out 20%, score it. R² 0.55. Presentable, and wrong.
About 60% of my data lists in 2025 to 26, after the IPO market cooled. Shuffling scatters those cooled-market IPOs across both the training and the test set, so the model gets to learn what the new market looks like from the very period I was testing it on. The jargon for it is regime leakage: not the answer leaking, but the era leaking. In production you never have that, you always predict forward into a market you have not seen.
So I switched to walk-forward validation and the score dropped to 0.50. Lower, and the only one worth quoting. The random-split number stays on the page as a warning, not as a result.
A few listings gain +90% and those outliers drag the model towards them, so the training target needs clipping at the 2nd and 98th percentile. I nearly clipped across the whole dataset, which would have quietly leaked the test period's distribution into training. Same error as the random split, harder to spot. It has to be clipped inside each fold, using only that fold's own training data.
The accurate version uses the final subscription book. That number only closes after bidding ends, which is after you have already applied, so it is fine for a paper and useless for a decision.
I rebuilt it using only what a retail applicant can see by bidding day 3. I expected to pay heavily for that and it cost almost nothing: R² 0.46 against 0.50. The deployed model is the day-3 one.
Every IPO has to be matched to the Nifty window around its own listing date. Get that join wrong by a few rows and the model is reading market data from after the listing, which looks like brilliant accuracy and is completely fake. Nothing errors out when this happens, which is what makes it dangerous. It is the step I checked most.
03 · Limitations
MAE is about 11 percentage points. A prediction of "+18%" means "probably a decent listing, give or take a lot". The app shows the error bar next to the number, because a model that hides its uncertainty is worse than no model.
That is the entire mainboard population for the window, not a sample I chose. But it caps how complex the model can usefully get.
SME IPOs behave differently and are not in the dataset, so the model should not be pointed at them.
The market regime shifts, that is the whole lesson of the project. It needs retraining as new listings land, roughly weekly.
The strongest feature comes from an unregulated, unaudited source. If GMP reporting changes, the model degrades and there is no clean substitute.
It predicts the listing pop, nothing after it. It says nothing about whether the stock is worth holding.
04 · What I learned
If the model predicts forward in production, it has to be scored predicting forward. Any validation that doesn't match the deployment loop is measuring something you will never actually do.
0.55 felt like progress and was the number I had to throw away. I now distrust a score that goes up without me having done anything to earn it.
Neither the shuffle nor the clipping threw an error. Both just returned a better number. The failures worth worrying about are the ones that look like success.
The final subscription book is genuinely predictive and genuinely unusable, because it arrives after the decision. Asking when a feature becomes available changed the model more than any tuning did.
Enter the pre-listing signals, issue size, early subscription, three-day GMP, Nifty mood, and the deployed retail model returns a listing-day gain with its honest error bar.