September 1, 2026 · corporate actions

Split-adjusted data, taken apart: what NVDA's 10-for-1 does to every row behind it

Split-adjusted data, taken apart: what NVDA's 10-for-1 does to every row behind it

Here is one row out of a daily equities file. NVDA, Friday 7 June 2024, the last session before the 10-for-1 split took effect.

ColumnValue
date2024-06-07
close1208.88
volume42,800,000 (rounded)
split_factor1.0
dividend0.00
adj_close120.8781

I've left the OHLC columns out of this view deliberately. They come back near the end, and they're the ones that quietly break.

Everything interesting about this row lives in the gap between column two and column six. The tape on 7 June printed 1208.88. Nobody will ever transact at that price again, in the sense that the share it referred to no longer exists as a unit. The 120.8781 is a reconstruction, and it is not simply 1208.88 divided by ten. Let's go column by column.

10:1divisor applied to every prior row
0.99991789dividend factor, stamped one day later
$45phantom commission per round trip
2.8ppannual drag invented by a divisor

date: 2024-06-07, and the two other dates that matter more

The split was announced on 22 May 2024 alongside earnings. The record date was 6 June. Trading began on a split-adjusted basis on Monday 10 June. Three dates, and your vendor's split_factor column sits on exactly one of them, usually the first split-adjusted session. Off-by-one here is the single most common corporate-action bug I see, and it's invisible in aggregate: your equity curve gets one 90% gap-down or one 900% gap-up on a single day, which a Sharpe computed over 2,000 days barely registers but a max-drawdown or a stop-loss rule absolutely does.

The dividend has its own date problem, and it changed underneath everyone in 2024. NVDA's post-split $0.01 quarterly dividend had a record date of 11 June. US settlement moved to T+1 on 28 May 2024, which means ex-date and record date are now the same day. Any loader that derives the ex-date as "record date minus one business day", which was correct for decades, started silently producing dividend adjustments one session early in June 2024. Nothing throws. The factor just lands on the wrong row.

The failure mode of corporate-action code is never an exception. It's a number that's off by a day and looks completely plausible.

close: 1208.88 — the price that isn't in the file

This is the raw print, and in most research pipelines it exists only as a column nobody reads. Signals get computed on adj_close. And any rule that references a per-share price level in dollars is now referencing a number no market participant ever saw.

NVDA's grind toward $1,000 in the spring of 2024 was a real thing with real order flow behind it: round-number magnets, options strikes clustered at 950 and 1000, headline risk on every approach. In adjusted data that entire episode happens at $100. If you're testing psychological-level behaviour, gap-fill logic near round numbers, or a strike-proximity feature, you're testing it against a price series that was rescaled by an event which hadn't happened yet.

The same applies to universe filters. "Exclude stocks under $5" is a legitimate proxy for a real cluster of microstructure problems: wide relative spreads, marginability rules, tick regimes. Run it against adjusted history and you exclude companies for periods when they traded at $30 and were later reverse-split. Run "price over $500" and you'll silently drop NVDA from every year before June 2024. Price filters belong on unadjusted prints. Full stop.

split_factor: 1.0 — and the 0.1 on the next row

The factor is a cumulative product running backward from today. Every row before 10 June 2024 carries a 0.1 from this split, multiplied by the 0.25 from the 4-for-1 in 2021, multiplied by whatever came before. NVDA's pre-2021 history is scaled by 0.025, so a bar that printed at $200 in 2019 shows up as $5.00.

Two consequences worth holding onto. First, the adjusted history changes every time a new corporate action lands. A backtest you ran in May 2024 and one you run today produce different numbers on identical code over identical dates, and if you cached features keyed on symbol and date without a data-version stamp, you now have a cache that mixes two scales. We version the adjustment epoch into the feature key for exactly this reason; it cost us one very confusing afternoon to learn.

Second, at 0.025 scaling, a stock that traded in $0.01 ticks now moves in increments of $0.00025. Your tick-rounding logic, your "is this a new high" comparisons, your float equality checks: all of them are now operating well below the resolution the market actually had.

dividend: 0.01 — the number that de-grids every price behind it

One cent. It looks like noise, and it's the reason adj_close reads 120.8781 rather than a clean 120.888.

The 10 June close was 121.79. The ex-dividend factor is (121.79 − 0.01) / 121.79 = 0.99991789. That factor multiplies every price before 11 June, so the split-adjusted 120.888 becomes 120.878074. Small, and permanent, and applied again at every one of the sixty-odd ex-dates going back through NVDA's history. The compounding is what matters: by the time you're looking at 2015, you're looking at a price that has been multiplied by dozens of irrational-ish factors. Not one adjusted historical price sits on the penny grid.

Which means every == comparison against a round level is false, every "did we touch the limit price" check needs a tolerance, and your reconstructed OHLC no longer satisfies the invariant that low ≤ open ≤ high after a naive round to 4 decimals. It also means adj_close is a total-return series while close is a price-return series, and if you compute returns from one and drawdowns from the other you've mixed two different definitions of what happened.

volume: 42,800,000 — the divisor nobody applies consistently

Share volume needs multiplying by 10 to be comparable with post-split sessions. Some vendors do it. Some don't. Some adjust the OHLC and leave volume raw, which is the worst of the three because the file looks internally consistent until you multiply price by volume.

The honest move is to stop using share volume as a feature and use dollar volume, which is invariant to splits by construction. A 20-day average dollar volume liquidity filter needs no adjustment logic at all and can't be broken by a corporate action. Feature invariance beats correct adjustment code, because correct adjustment code is a thing you have to keep being right about.

Three shapes of vendor file, three failure modes.

What shipsWhat breaks
Adjusted close only, raw OHLCGap and intrabar logic computes across two price scales
Adjusted OHLC, raw volumeDollar volume off by the split ratio; ADV filters misfire
Raw OHLCV plus factor columnsNothing, if you apply them. Everything, if you apply them on the wrong row

The columns that aren't here: adj_open, adj_high, adj_low

Now put the OHLC back. If a feed adjusts only the close and your strategy computes an overnight gap as open / prev_adj_close − 1, then on 10 June 2024 it computes 120.37 / 120.8781 − 1, which is a fine number, roughly −0.4%. Push the same code back to 7 June 2024 and it computes the open from the raw file (about 1207) against a close adjusted by 0.025 from older splits, and it produces a gap of several thousand percent.

Anything that mixes a raw column with an adjusted one is arithmetic across two coordinate systems. I now treat mixed-adjustment reads as a loader-level assertion rather than something a researcher has to remember, because researchers won't, including me.

What no column tells you: how many shares you'd actually have bought

This is the one that costs money in the backtest's cost model, and it's the reason I'm writing about a stock split at all.

US equity commissions are frequently per share. Call it $0.005 with a $1.00 order minimum, which is a common retail-broker fixed tier. Now backtest a $400,000 NVDA position a year before the split, when the raw price was near $400 and the adjusted price shows as $40.

SharesCommission
What actually happened (raw $400)1,000$5.00
What the backtest sees (adjusted $40)10,000$50.00

Same exposure, same trade, 10x the modelled cost. That's $45 of fiction per leg, $90 per round trip, and a strategy turning over daily eats about $22,000 a year of costs that never existed. On a $400k book, 5.6 percentage points of annual return, entirely manufactured by a divisor. Here it makes you look worse than reality, which is survivable.

Reverse splits run the other way, and that's the dangerous direction. A 1-for-10 reverse split means adjusted history shows prices 10x higher and share counts 10x lower than what you'd have traded. Your cost model quietly divides the per-share commission by ten. The universe where reverse splits are common is the small-cap, sub-$5, wide-spread universe where per-share fees are the largest single line item in the cost stack. The backtest flatters exactly the names it should be punishing.

So the fix is boring and structural: size positions in dollars, convert to shares using the unadjusted price for that date, and charge per-share fees on that count. Keep the raw price column in the loader forever, not as an audit artifact but as a live input to the fill engine.

  1. Signals and returns: adjusted series.
  2. Share counts, per-share fees, tick rounding, price-level filters: unadjusted series.
  3. Liquidity and capacity: dollar volume, which needs neither.

Six columns, one row, one fairly ordinary corporate action that was announced three weeks in advance and surprised nobody. The market handled it in a morning. Reconstructing it faithfully in a research pipeline is a good deal harder than that, and the tell that a pipeline hasn't is almost never an error message.

corporate actionssplit adjustmentus equitiesdata engineeringbacktesting
← All posts