2. Price versus total return: two different series, two different conclusions
Level 3: Adjustments and corporate actions
The previous module treated the adjustment as a technical correction. This one deals with the decision behind it, because there is no single correct series: there are two, they measure different things and they answer different questions. Choosing badly does not produce a 0.1% error; it produces differences that over twenty years are counted in multiples.
Two questions, two series
A price series answers this: what is the security quoted at? It is the series you see on the screen, the one you use to know what price you would get in at today and the one you need in order to compute a commission, a slippage figure or the size of an order.
A total return series answers something else: by how much has the money of somebody who bought and reinvested everything they were paid multiplied? Its levels are not tradable prices. They are the value of an imaginary portfolio that started with one security and kept buying additional fractions every time a dividend came in.
The confusion arises because both arrive in a column called price and looking exactly alike. But only the second is a measure of performance.
import pandas as pd
px = pd.Series(
[100.0, 101.0, 99.5, 100.5, 103.0],
index=pd.date_range("2024-01-02", periods=5, freq="B"),
)
div = pd.Series(0.0, index=px.index)
div.iloc[2] = 1.20 # dividend with ex date on the third day
r_precio = px.pct_change()
r_total = (px + div) / px.shift(1) - 1
print((1 + r_precio).prod() - 1) # 0.0300 -> +3.00%
print((1 + r_total).prod() - 1) # 0.0424 -> +4.24%Five sessions and there are already 124 basis points of difference. The price series is not miscalculated: it is measuring something else. What is wrong is calling what it returns a rate of return.
How far apart they drift over the long run
The arithmetic is merciless because dividend yield does not add, it compounds. Suppose a price index that advances 8.0% a year and its total return version that advances 10.0%: two points of difference, which sounds like very little.
anios = 30
print(1.08 ** anios) # 10.06 times
print(1.10 ** anios) # 17.45 times
print(1.10 ** anios / 1.08 ** anios) # 1.734After thirty years, the version with dividends is worth 73% more. Not 2% more nor 60% more: 73%, and it grows the longer the horizon. In markets with a high dividend yield (the UK market, much of the European one, utilities, oil majors, listed property funds) the gap is larger still, because a very big part of the shareholder's return never shows up in the price: it leaves the company as cash and the price, by construction, falls to reflect it.
That is the point to internalize. A dividend is not an extra added on top of the price rise: it is a part of the return that has been subtracted from the price. The price series of a company that pays out all its earnings and does not grow can spend decades flat while its shareholder doubles their money. The series is not lying. It simply is not measuring what you think.
Rigged comparisons
This is where it stops being a debate about definitions and becomes a backtest that passes a bad strategy or fails a good one.
- Price-based strategy against a total return index. You penalize yourself by between one and four points a year depending on the market. A strategy that draws level ends up looking like a disaster.
- Dividend-adjusted price strategy against a price index. The opposite error, and a far more dangerous one because it produces alpha out of thin air. All your stocks collect dividends and the benchmark does not.
- A basket tilted toward dividend yield measured on prices. The bias is twofold: you have picked precisely the stocks whose return does not show up in the price.
The rule is easy to state and easy to skip: numerator and denominator have to be built on the same convention. If you do not know which one your benchmark index uses, find out before drawing conclusions, not afterwards.
def comparar(estrategia, referencia, tipo_estrategia, tipo_referencia):
# Abort if conventions get mixed: that is a data fault, not a strategy fault.
if tipo_estrategia != tipo_referencia:
raise ValueError(
"Convenciones distintas: " + tipo_estrategia + " vs " + tipo_referencia
)
return (1 + estrategia).prod() - (1 + referencia).prod()A real case: the DAX and its invisible brother
Germany's DAX is the textbook example because it works the opposite way round from almost every other index. The one that gets published and appears in the headlines is a total return index: it incorporates reinvested dividends. There is also a less widely circulated price version, the DAX Kursindex, which does not. In everyday conversation both go by the same name, and they have been drifting apart for decades.
The practical consequence is direct. If you backtest a strategy on the closing prices of German stocks and compare it with the DAX you downloaded without looking, you are competing against a rival that collects dividends while you do not. With an average yield on the order of 3% a year, thirty years of that advantage is a factor of 1.03 to the power of 30, around two and a half times. No reasonable strategy overcomes that kind of handicap, and not because it is a bad one.
The reverse case exists too and is more common: the S&P 500 and the Nikkei 225 you see in the headlines are price indices. Comparing yourself against them using dividend-adjusted prices hands you an advantage you have not earned.
To go deeper into this lesson
- Total returnin the glossary