The split that broke our adjusted prices
A reviewer told us to walk Nestle's 10:1 split and check for a discontinuity. There was one. Three bugs later, the Yahoo reconciliation went from 88.79% to 96.88%, and the two names still off are Yahoo's.
On 8 September we got two outside reviews of TejHQ in one afternoon. The second one ended with a checklist for anyone thinking of trusting the data, and item three was the sharp one: take a known split, walk the adjusted series across the ex date, and look for a discontinuity. We ran it. It failed.
This is the build log of what that one test found, which turned out to be three separate defects in the adjustment pipeline, and what the numbers look like now that they are fixed. Everything below is reproducible from the public parquet with the commands at the end.
The test
Nestle India split 10:1 on 5 January 2024. The raw close went from 27,116.40 to 2,666.40 overnight, which is what a split looks like. The adjusted close is supposed to not notice. Ours noticed:
date close adj_factor adj_close
2024-01-04 27116.40 0.10000 2711.64
2024-01-05 2666.40 0.48775 1300.53 <- a 52% drop that never happenedThe factor on the left of the split is exactly 0.1. That is the split, and nothing else. Every later action on Nestle, a 1:1 bonus and two years of dividends, reached the rows after 5 January and none of the rows before it. The adjusted series has two halves that were never joined.
Bug one: the ISIN that changed
In India a face value split issues a new ISIN. Nestle traded as INE239A01016 until 4 January and as INE239A01024 from the 5th. The bhavcopy switches on the ex date. Our back-adjustment, and the metrics windows, and the liquidity universe, all partitioned by ISIN, because ISIN is the stable identifier and symbols are not. Except that here the symbol was stable and the ISIN was not.
We counted. Since 2010, 591 NSE symbols have moved to a new ISIN under the same ticker, 582 of them within a week, which is the face value change pattern. Tata Steel in July 2022, HDFC Bank in 2011 and again in 2019, most of the large caps at some point. Every one of them had a broken adjusted series, a 52 week window that restarted from nothing, and a three month hole in the point-in-time universe while the new ISIN accumulated enough history to rank.
The fix is an instrument chain: two ISIN intervals belong to the same instrument when the same symbol moved from one to the other with at most 30 calendar days between the last day of the old and the first day of the new. A ticker that vanished for a year and came back on a fresh ISIN is a different company, which also covers ticker reuse. Every derived step now partitions by the chain. The isin column in every published file still says exactly what the exchange said that day.
# Consecutive intervals of the same symbol on different ISINs,
# close enough in time to be the same instrument.
links = ordered.filter(
pl.col("_prev_isin").is_not_null()
& (pl.col("_prev_isin") != pl.col("isin"))
& ((pl.col("valid_from") - pl.col("_prev_to")).dt.total_days() <= MAX_GAP_DAYS)
)
# Union-find over the links; the chain id is the ISIN with the latest trading day.With that in place the split test passes, and Nestle's split-day return in the metrics tree is the real minus 1.67% instead of minus 90%. But we had a reconciliation tool, so we ran it.
Bug two: the step on 1 January
The reconciliation compares our adjusted close with Yahoo Finance's for the 50 most traded NSE names over 2024 to May 2026. The chain fix moved the headline from 88.79% to 89.05%, which is nothing, because large caps rarely change ISIN in a two year window. So the 11% residual was something else. TCS was at 12.5% within tolerance, ITC at 14%, Infosys and HCL at 57%. Dividend payers.
The useful view is not the level but the steps: on which dates does each series change its adjustment factor, and by how much. For TCS, nine of eleven dividend steps matched Yahoo to six decimal places. Two dates did not, and two dates had a step on our side with no event at all:
date ours yahoo event
2024-01-19 0.002311 0.006967 dividend (see bug three)
2024-05-16 0.007268 0.007268 dividend, Rs 28
2025-01-01 -0.006817 0.000000 nothing happened
2026-01-01 -0.005470 0.000000 nothing happened
2026-05-25 0.013559 0.013559 dividend, Rs 31A step on 1 January with no event is a year boundary, and the pipeline builds adjusted prices one year file at a time. A dividend factor is (prev_close - cash) / prev_close, where prev_close is the close on the session before the ex date. The 2024 build looked that close up inside the 2024 price slice. For a dividend in June 2025, the lookup found the last row it had, 31 December 2024, and priced the dividend off that. TCS fell from 4,100 toward 3,000 across 2025, so every 2025 and 2026 dividend was under-weighted in the 2024 rows by the ratio of those prices. The cumulative error walks in with every year of history. The lookup now spans the full price history, and the January steps are zero.
Bug three: the dividend that was two dividends
The remaining mismatch on 19 January 2024. The NSE announcement reads:
Interim Dividend - Rs 9 Per Share Special Dividend - Rs 18 Per ShareOur parser took the first amount. Rs 9 went into the factor. Yahoo's step of 0.006967 is Rs 27 on the previous close, which is the right answer, because both payouts went ex the same day and the price dropped by both. Of 20,683 dividend announcements in the NSE feed, 703 carry more than one amount: a final plus an interim, a special dividend, an InvIT paying interest plus return of capital plus dividend. The parser now sums them, with one exception for announcements that state a total and then break it down, where the first amount already is the total. A new actions reparse command rewrote the stored files from the text they already carried, 471 rows changed, and it runs in the cron before every adjust.
What the reconciliation says now
Same 48 symbols, same 25,329 daily comparisons, same 1% tolerance:
before after
overall 88.79% 96.88%
TCS 12.52% 99.83%
INFY 57.22% 99.83%
HCLTECH 55.13% 100.00%
TATASTEEL 57.22% 99.83%
RECLTD 57.22% 99.83%
SHRIRAMFIN 57.22% 99.65%
ITC 14.26% 56.87% <- Yahoo, see below
TRENT 14.26% 14.26% <- Yahoo, see belowForty-six of forty-eight names sit between 98.96% and 100%, with mean differences in the thousandths of a percent. The per-symbol table is in reconcile_report.md and is regenerated with the commands at the bottom.
What Yahoo gets wrong
Two names did not move, and this time the step view points the other way.
- TRENT. Yahoo applies Trent's 1:2 bonus on 1 January 2026. The exchange ex date is 4 June 2026, which is where our raw close drops 33% and Yahoo's does not. For five months every row differs by the bonus factor. Yahoo has the event and the wrong date.
- ITC. ITC demerged its hotels business in January 2025. Yahoo scales the history before the ex date by about 3.7% for it. We follow the NSE convention and leave demergers, rights, buybacks and mergers unscaled. Neither is wrong; they are different conventions, and ours is now written down.
The previous version of this reconciliation attributed the residual to "the dividend formula". That was a guess, and it was wrong. Yahoo's dividend factor, one minus cash over the previous close, is algebraically identical to the NSE one. The residual was ours, and it took a test with a known answer to see it.
One more thing changed on the way: Yahoo's chart endpoint now refuses requests without a session cookie, whatever the pace, so the reconcile command talks to it through yfinance.
What changed for you
prices_adjusted/,metrics/anduniverse/on HuggingFace and R2 were rebuilt on 8 September with all three fixes. If you pulled them before that, pull again.actions/has correctedcash_amounton 471 NSE rows. Raw bhavcopy is untouched, it never was the problem./v1/adjusted,/v1/metrics,/v1/universeand/v1/screenerserve the rebuilt trees. Nothing changed in the API shape.
Reproduce the reconciliation from a clean checkout:
git clone https://github.com/tejhq/tej-bazaar && cd tej-bazaar
pip install -e '.[reconcile]'
tej-bazaar pull-r2 --prefix nse/ --prefix actions/ --data-dir data/out --bucket tej-bazaar
tej-bazaar actions reparse --actions-dir data/out/actions
tej-bazaar actions adjust --all-years -e nse --prices-dir data/out \
--actions-dir data/out/actions --out-dir data/derived/prices_adjusted
tej-bazaar reconcile --from 2024-01-01 --to 2026-05-06 -e nse --top 50 \
--adjusted-dir data/derived/prices_adjusted --tolerance 1.0The reviewer's checklist had five items. Raw OHLCV against the exchange files, missing trading days, duplicate rows, corporate action coverage by type, and a timestamp on all of it, are the ones we have not published yet. That is the next thing, and when it ships it will be a page the cron rewrites every night, not a number in a blog post. If you have a split, a bonus or a merger you think we get wrong, the fastest way to find out is to send the symbol to github.com/tejhq/tej-bazaar. The test that found these three took one curl.