sifting/io
Quant Research & Backtesting
8 min readSiftingIO Team

Adjusted vs unadjusted stock prices: total return vs price return in a backtest

Build a total-return series from as-traded prices, split factors, and cash dividends with reinvestment math, and see where the adjusted-close shortcut misleads a backtest.

Adjusted vs unadjusted stock prices: why your backtest returns are wrong

Adjusted vs unadjusted stock prices decide whether a backtest measures what a holder actually earned or something merely close to it. The familiar failure is the split: AAPL closed at 499.23 on August 28, 2020 and at 129.04 on August 31, 2020, the ex-date of its 4-for-1 split, and on unadjusted bars that reads as a 74.2% loss in one session. The less familiar failure is quieter. A backtest run on the adjusted close column assumes every dividend was reinvested, gross of tax, on the ex-date, at a price that never printed. For many strategies that assumption is close enough. For others it's the difference between a result that reconciles to a brokerage statement and one that doesn't.

This post builds a total-return series from as-traded prices, split factors, and cash dividends with explicit reinvestment, then compares it to the adjusted-close shortcut so you can see where the two diverge. It assumes you already know that a split factor of 0.25 rescales every price before the ex-date. If the fake-crash failure is new to you, why two backtests disagree covers it first.

Adjusted vs unadjusted stock prices: what each series encodes#

Three series describe one stock, and a backtest usually needs two of them.

The as-traded (unadjusted) close is what a share cost on that date. It's the only series with a physical meaning: converting cash into a share count, checking a round-lot rule, or matching a price quoted in an 8-K.

The split-adjusted close divides every pre-split price by the split ratio and leaves dividends alone. Returns computed from it are price returns: what the share price did, ignoring cash paid out. It's the right series for anything that should not credit dividends, such as an options-hedging study or a strategy that sweeps dividend cash out of the account.

The fully adjusted close applies both split factors and dividend factors. The dividend factor for an ex-date is 1 minus the cash per share divided by the prior as-traded close. AAPL went ex-dividend on November 6, 2020 paying 0.205 per share against a prior close of 119.03, so the factor is 1 - 0.205 / 119.03, about 0.99828, applied to every bar before that date. Returns from this series are approximately total returns. The word approximately is doing real work.

Work the ex-date return both ways. From the adjusted close, the return is P_ex / (P_prev x (1 - D / P_prev)) - 1, which simplifies to P_ex / (P_prev - D) - 1. The one-day total return for a holder who received the cash is (P_ex + D) / P_prev - 1. The two agree only when P_ex equals P_prev - D exactly, meaning the stock fell by precisely the dividend and nothing else happened. On any other day they differ by a small amount, and that amount recurs on every ex-date in the history. For a quarterly payer that's forty small discrepancies over a decade. Each is second order. The sum usually stays within a few basis points, and usually is a weak foundation for a number you intend to quote.

The larger gap is what total return means. The adjusted close reinvests the gross dividend on the ex-date at P_prev - D. A real holder receives the cash on the pay date, which for that AAPL dividend was November 12, 2020, sometimes net of withholding tax, and reinvests it at whatever price is available then, if at all. A backtest that reports total return from the adjusted close is reporting one specific reinvestment convention. Either make that convention explicit or replace it with the one your strategy actually follows.

Building a total-return series with reinvestment math#

Keep the as-traded bars untouched and simulate a single share through the corporate-actions list. On a split ex-date, multiply the share count by the ratio. On a dividend ex-date, compute the cash owed (shares held times cash per share) and either reinvest it immediately or hold it until the pay date. The index on any day is shares times the as-traded close plus any cash not yet invested.

import pandas as pd

def total_return_index(bars, actions, reinvest_on="pay_date"):
    """bars: as-traded daily OHLCV indexed by date, close in column c.
    actions: splits {ex_date, ratio} and dividends {ex_date, pay_date, cash}."""
    shares, cash, pending = 1.0, 0.0, []
    out = pd.Series(index=bars.index, dtype="float64")
    for day, row in bars.iterrows():
        for a in actions:
            if pd.Timestamp(a["ex_date"]) == day:
                if a["type"] == "split":
                    shares *= a["ratio"]
                else:
                    pending.append((pd.Timestamp(a[reinvest_on]), shares * a["cash"]))
        cash += sum(amt for when, amt in pending if when <= day)
        pending = [(when, amt) for when, amt in pending if when > day]
        if cash:
            shares += cash / row["c"]
            cash = 0.0
        out[day] = shares * row["c"]
    return out / out.iloc[0]

actions = [
    {"type": "split", "ex_date": "2020-08-31", "ratio": 4.0},
    {"type": "dividend", "ex_date": "2020-11-06", "pay_date": "2020-11-12", "cash": 0.205},
]
total = total_return_index(bars, actions)
price_only = total_return_index(bars, [a for a in actions if a["type"] == "split"])
daily_total_returns = total.pct_change()

Passing reinvest_on="ex_date" reproduces the adjusted-close convention to within the P_prev - D versus P_ex difference described above. Passing the split-only actions list gives the price-return index, and the ratio of the two series is the dividend contribution over the sample. That contribution is not small over long windows. A stock yielding 0.6% a year compounds to about 3% over five years, and a 3% yielder over ten years compounds to about 34%, enough to reorder a ranking of strategies scored on price return versus total return.

In a multi-position simulator the dividend is simply a cash credit on the pay date. The strategy then buys whatever its rules say to buy next, which is rarely the same stock. That's the reinvestment convention no adjusted-close column can express.

Special dividends and volume adjustment#

Special dividends break the assumption that a dividend factor is close to 1. MSFT paid a 3.00 per share special dividend with an ex-date of November 15, 2004, when the share price was near 30, so the factor was about 0.90 versus about 0.998 for a routine quarterly payment. Three things follow. A split-adjusted price-return series shows a 10% drop that day, which is correct as a price return and wrong as a measure of what the holder experienced. A total-return build with the special missing from its actions list shows the same phantom loss. And adjusted-close columns don't all treat specials the same way, since folding them into the factor chain and adjusting only for regular dividends are both conventions in use. Check which one your source follows on a date where you know a special was paid.

Volume needs its own adjustment, and only for splits. Share count changes on a split, so volume before the ex-date is multiplied by the ratio to stay comparable. Dividends leave volume alone. Dollar volume, price times volume, is invariant as long as both sides use the same coordinate system: 499.23 times V and 124.81 times 4V are the same number. Mixing them is the trap. Unadjusted volume next to a fully adjusted close understates pre-split dollar volume by the split ratio, 4x for AAPL, so an average-dollar-volume filter can drop a stock from its own pre-2020 history. Any table with an adjusted price column beside raw volume has this problem by construction.

Pulling as-traded bars from a historical stock prices API#

Daily bars spanning both AAPL actions above:

curl --compressed -H "X-API-Key: $SIFTING_KEY" \
  "https://api.sifting.io/v1/hist/stocks/AAPL/bars?interval=1d&start=2020-08-24&end=2020-11-13"

Historical bar endpoints require gzip. Without an Accept-Encoding: gzip header the API returns 406 gzip_required; curl's --compressed flag sets the header and decompresses for you. The endpoint takes interval, start, end, limit, and cursor; the historical stocks docs list them. Confirm whether the series you pulled is as-traded or already adjusted before feeding the close into the reinvestment code, because that decides whether the corporate actions still need applying or are already baked in.

Store the as-traded bars, the actions list, and the meta.as_of timestamp from the response. The adjusted close for 2015 changes every time a new dividend goes ex, so a backtest cached on the adjusted column won't reproduce next quarter. As-traded prices plus a dated actions list will.

Common pitfalls#

Double-counting dividends. A simulator that credits dividend cash on the ex-date and also marks positions at the fully adjusted close pays every dividend twice. Feed the simulator as-traded prices, or at most split-adjusted ones.

Computing the dividend factor from an already-adjusted prior close. The factor is 1 - D / P_prev with P_prev as traded. A 2015 AAPL quarterly dividend of 0.52 against an as-traded close near 130 gives a factor near 0.996; the same cash against the split-adjusted close near 32.5 gives near 0.984, a 1.2 percentage point error on a single dividend, repeated for every dividend before the split.

Using the wrong date. The ex-date is when the price drops and is the date that pairs with the factor. Triggering the share-count change on the pay date or the record date shifts reinvestment by days, and if a rebalance falls in between, cash lands on the wrong side of it.

The free tier covers one month of history, which is enough to verify the split test on a known ex-date before committing to a longer pull. Start building free.

Keep reading

Related posts