Strasmore Research
Deep Dives · Matt ConnorBy Matt Connor ·

How split-adjusted price history works

Split-adjusted price history explained: the 1/N factor charts apply to old prices, the N they apply to old volumes, and the three ways it breaks a backtest.

Split-adjusted price history is the arithmetic a stock chart applies to its own past without announcing it. For an N-for-1 split every prior price is divided by N and every prior share count is multiplied by N, so a $600 print from before a 10-for-1 split reads as $60 on ten times the shares from that day forward. Dollar volume comes out unchanged, and so does the percentage shape of the line. Only the axis labels move.

What a split adjustment does to the numbers

A stock split hands existing holders more shares at a proportionally lower price. What a stock split is covers the corporate side. This post covers what happens to the price file afterward.

The whole adjustment is one number. Call it the split multiple N, the count of new shares per old share. Every open, high, low, close, and cash dividend dated before the effective session is divided by N. Every share count before that session is multiplied by N. Nothing on or after the effective session is touched. A reverse split runs the same machinery with N below 1: a 1-for-15 reverse means N is 1/15, and dividing prior prices by 1/15 is the same as multiplying them by 15.

The panel below lists the large splits household names have run since 2020, with the multiple stated as a plain number. Read that column as both instructions at once: divide the old prices by it, multiply the old volumes by it.

QuerySplit multiples for large US stock splits since 2020
The exact SQL behind every number
SELECT
    concat(ticker, ' ', formatDateTime(execution_date, '%b %Y'))                             AS split_event,
    concat(toString(toUInt32(any(split_to))), '-for-', toString(toUInt32(any(split_from)))) AS split_ratio,
    round(toFloat64(any(split_to)) / toFloat64(any(split_from)), 2)                          AS split_n
FROM global_markets.stocks_splits
WHERE execution_date >= '2020-01-01'
  AND execution_date < today()
  AND split_to >= split_from * 3
  AND ticker IN ('AAPL', 'AMZN', 'CMG', 'GOOGL', 'NVDA', 'SHOP', 'TSLA', 'WMT')
  AND ticker NOT IN ('SPCX')
GROUP BY ticker, execution_date
ORDER BY split_n, split_event
Run this yourself

That set holds 10 splits, running from a multiple of 3 at the small end up to 50 at CMG Jun 2024. Take any close from the session before that last one, divide it by 50, and you have the figure a chart draws for that session today. The date to pivot on is the ex-split session, the first day the shares trade at the new price. When a stock split takes effect walks that calendar, and the distance between the announcement and the effective session is where most off-by-one adjustment bugs live.

How can I tell if a price series is already split-adjusted?

One query settles it. Take the last close before the effective session and divide it by the first close on or after it. A quotient near 1.0 means the prior rows already carry the 1/N factor. A quotient near N means the file stores prices as they printed on the day, with the adjustment left to you.

QueryIs the price file already adjusted? The pre and post close quotient
The exact SQL behind every number
WITH
sp AS (
    SELECT
        ticker,
        execution_date,
        toFloat64(any(split_to)) / toFloat64(any(split_from)) AS n
    FROM global_markets.stocks_splits
    WHERE execution_date >= '2020-01-01'
      AND execution_date < today() - 20
      AND split_to >= split_from * 3
      AND ticker IN ('AAPL', 'AMZN', 'CMG', 'GOOGL', 'NVDA', 'SHOP', 'TSLA', 'WMT')
    GROUP BY ticker, execution_date
),
px AS (
    SELECT
        ticker,
        date,
        toFloat64(any(close)) AS c
    FROM global_markets.stocks_daily_aggs
    WHERE ticker IN ('AAPL', 'AMZN', 'CMG', 'GOOGL', 'NVDA', 'SHOP', 'TSLA', 'WMT')
      AND date >= '2019-12-01'
      AND date < today()
    GROUP BY ticker, date
)
SELECT
    concat(sp.ticker, ' ', formatDateTime(sp.execution_date, '%b %Y'))  AS split_event,
    round(any(sp.n), 2)                                                 AS split_n,
    round(argMaxIf(px.c, px.date, px.date <  sp.execution_date)
        / argMinIf(px.c, px.date, px.date >= sp.execution_date), 3)     AS close_ratio
FROM sp
INNER JOIN px ON px.ticker = sp.ticker
WHERE px.date >= sp.execution_date - 15
  AND px.date <= sp.execution_date + 15
GROUP BY sp.ticker, sp.execution_date
HAVING countIf(px.date <  sp.execution_date) > 0
   AND countIf(px.date >= sp.execution_date) > 0
ORDER BY close_ratio DESC, split_event
Run this yourself

The panel runs that test on 10 split events. The highest quotient sits at 1.06 for SHOP Jun 2022, whose split multiple was 10, and the lowest is 0.888. Compare the two numeric columns bar by bar and the file's convention is unmistakable, with no field-name documentation required. The same ticker can carry one convention in a charting product and another in a bulk download, so the test is worth running per source rather than once.

What survives the adjustment

Two quantities pass through a split adjustment untouched. Dollar volume is the first: price falls by N while shares rise by N, and the product holds. Percentage change is the second: it is a ratio of two adjusted prices, and the factor cancels top and bottom.

That makes dollar volume a free integrity check. The panel below pins one stock across a large forward split, over a window of past sessions that will never refresh.

QueryDollar volume and daily range across a large forward split (NVDA, 2024)
The exact SQL behind every number
SELECT
    toString(date)                                                  AS session_date,
    formatDateTime(date, '%b %e')                                   AS session_label,
    round(toFloat64(any(close)) * toFloat64(any(volume)) / 1e9, 2)  AS turnover_billions,
    round(100 * (toFloat64(any(high)) - toFloat64(any(low)))
        / toFloat64(any(close)), 2)                                 AS range_pct
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'NVDA'
  AND date >= '2024-05-28'
  AND date <= '2024-06-24'
GROUP BY date
ORDER BY date
Run this yourself

The split took effect on Jun 10. Turnover measured $49.84 billion on Jun 7, the session before, against $38.26 billion on the effective session itself. A file that scaled prices and share counts together shows no step in the turnover column at the boundary. A file that scaled one side only shows a step of roughly N times, which is the fastest way to catch a half-applied adjustment.

The range column on that same session carries a warning of its own. High minus low on the effective session reads 64.82% of the close, a span far outside what an ordinary session in a megacap prints. A number like that, landing on the exact session a factor changes, is a print to reconcile against a second source before any volatility screen or gap filter reads it as trading. The habit generalizes past this one file: the session a factor lands on is worth a manual look, whatever the vendor.

The dividend adjustment: total return series

A total return series applies a second factor, on every ex-dividend date rather than on split dates. Multiply every prior price by (1 - D / C), where D is the cash amount per share and C is the close on the session before the stock goes ex. Each factor is small. Stacked over years, they are not.

QueryDividend adjustment factors on every KO ex-date since 2019
The exact SQL behind every number
WITH
px AS (
    SELECT
        date,
        toFloat64(any(close)) AS c
    FROM global_markets.stocks_daily_aggs
    WHERE ticker = 'KO'
      AND date >= '2018-10-01'
      AND date < today()
    GROUP BY date
),
lagged AS (
    SELECT
        date,
        lagInFrame(c) OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prior_close
    FROM px
),
divs AS (
    SELECT
        ex_dividend_date            AS d_date,
        toFloat64(max(cash_amount)) AS cash
    FROM global_markets.stocks_dividends
    WHERE ticker = 'KO'
      AND ex_dividend_date >= '2019-01-01'
      AND ex_dividend_date < today()
      AND cash_amount > 0
    GROUP BY ex_dividend_date
)
SELECT
    toString(divs.d_date)                           AS ex_date,
    formatDateTime(divs.d_date, '%b %e, %Y')        AS ex_label,
    round(100 * divs.cash / lagged.prior_close, 3)  AS dividend_pct,
    round(100 * (1 - exp(sum(log(1 - divs.cash / lagged.prior_close))
        OVER (ORDER BY divs.d_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW))), 2) AS cumulative_pct
FROM divs
INNER JOIN lagged ON lagged.date = divs.d_date
WHERE lagged.prior_close > 0
ORDER BY divs.d_date
Run this yourself

Coca-Cola's first ex-date in the panel, Mar 14, 2019, carried a cash amount worth 0.865% of the prior close. Compounding every ex-date through Jun 15, 2026 gives a cumulative adjustment of 20.43%. A price-only history and a total return history of the same stock start that far apart at the left edge of the chart, before a single price move is counted. This is the plain answer to a question that comes up constantly: a historical price on one site does not match a press release or an old brokerage statement, and neither figure is wrong. They apply different factor stacks to the same tape.

Where split adjustment breaks a backtest

  1. Adjusting price without adjusting volume. Dollar volume then jumps by N on the ex-split session, and any liquidity screen or turnover filter reading that day fires on an artifact rather than on trading.
  2. Applying the factor on or after the ex-split session instead of strictly before it. One session of slippage leaves a fake N-fold gap in the return series, and a gap or momentum filter will happily trade it.
  3. Comparing an adjusted close against an unadjusted threshold. A $1 exchange listing minimum, a $5 institutional price floor, an index inclusion rule, and an option strike are all quoted in the prices of their own day. An adjusted series that reads $3 for a session that printed $45 will trip a penny-stock filter that never applied at the time.

The last one is a close cousin of look-ahead bias in backtesting: an adjusted file encodes corporate actions that had not happened yet on the session it describes. Any rule stated in dollars needs the price as printed. Any rule stated in percentages can use the adjusted series.

Run the factor arithmetic yourself (standard-library Python, nothing to install)
from decimal import Decimal, ROUND_HALF_UP

CENT = Decimal('0.01')

def cents(x):
    return x.quantize(CENT, rounding=ROUND_HALF_UP)

# A hypothetical daily history: session label, close, shares traded.
raw = [
    ('day 1', Decimal('599.99'), 1000000),
    ('day 2', Decimal('612.37'),  900000),
    ('day 3', Decimal('594.05'), 1200000),
]

forward = Decimal(1) / Decimal(10)   # 10-for-1 split: prior prices times 1/10
reverse = Decimal(15)                # later 1-for-15 reverse: prior prices times 15

header = ('label', 'raw', 'stepwise', 'one-shot', 'drift', 'adj shares')
print('{:>6}{:>10}{:>10}{:>10}{:>8}{:>12}'.format(*header))

for label, close, shares in raw:
    stepwise = cents(cents(close * forward) * reverse)
    one_shot = cents(close * forward * reverse)
    adj_shares = Decimal(shares) / forward / reverse
    print('{:>6}{:>10}{:>10}{:>10}{:>8}{:>12.2f}'.format(
        label, close, stepwise, one_shot, stepwise - one_shot, adj_shares))

The stepwise column rounds to the cent after each corporate action. The one-shot column applies the composite factor once and rounds at the end. On whole-number ratios the two agree exactly. On ratios like 1-for-15 they part company by a cent or two per row, and the share counts stop landing on integers. A series rebuilt in one pass rarely ties out to the last penny against a series that was adjusted event by event, which is normal rather than a defect in either one.

How options handle a split differently

Options run their own convention, worth knowing before anyone compares a strike against an adjusted chart. For a whole-number split the standard route multiplies the number of contracts and divides the strike, leaving the deliverable at 100 shares. For a ratio that is not a whole number, and for reverse splits, the strike stays where it is and the deliverable changes instead, so a single contract can end up covering an odd share count plus cash in lieu. Either way, a strike printed on an older contract is a number from the world before the adjustment. How stock splits affect options has the contract adjustment detail.

FAQ

What does split-adjusted price mean?

A split-adjusted price is a past price restated in today's share terms. For an N-for-1 split, every price before the effective session is divided by N and every share count is multiplied by N, so the chart runs continuously instead of showing a cliff on the split date.

Why doesn't a historical price match an old press release?

The press release quotes the price as it printed that day. A chart quotes the same session after every split factor since then has been applied, and a total return chart also applies a dividend factor for every ex-date. Both figures describe the same trade, stated in different units.

What is the difference between split-adjusted and total return prices?

Split adjustment applies only the share-count factors. A total return series applies those factors and then a (1 - D / C) factor on every ex-dividend date, which restates the history as though each dividend had been reinvested at the close.

How do I convert an adjusted price back to what actually printed?

Multiply the adjusted price by every split multiple that has taken effect since that session. One 10-for-1 followed by one 2-for-1 means multiplying by 20. Dividend adjustments cannot be unwound this way without the full ex-date and cash-amount history.

Does a split change what a holding is worth?

Not on its own. The share count and the price move by exactly offsetting factors, which is the property the whole adjustment scheme rests on. Any price change around a split date comes from trading, not from the arithmetic.


Every panel here carries the exact SQL that produced it, expandable underneath. To check whether a price series you rely on already carries the split factor, ask the question in plain English on the Strasmore terminal.