Lesson 1.4.6 · 60 min
Continuous futures series: roll rules, difference adjustment and ratio adjustment
A futures contract lives a few months and then expires; a backtest needs years. The continuous series splices successive contracts, and each way of doing it preserves one thing and distorts another. This lesson builds the three usual versions with the 31 contracts of the fictitious FIX future and checks which one tells the truth about results.
- Module
- 1.4 · Financial data engineering
- Duration
- 60 min
- Prerequisites
- 1.2.3 · Futures: specifications, margin, roll and contango, 0.5.3 · Selection, filtering, groupby, merge, pivot, resample and moving windows
- Author
- Jesús Cuesta
- Updated
Level 1 lessons
1.1 · How markets work
1.2 · Asset classes
- 1.2.1 Stocks: splits, dividends and corporate actions
- 1.2.2 ETFs: replication, tracking error, leveraged ETFs and UCITS
- 1.2.3 Futures: specifications, margin, roll and contango
- 1.2.4 Forex: pairs, pips, lots, rollover and CFDs
- 1.2.5 Options: calls, puts, intrinsic value and time value
- 1.2.6 Crypto: spot, perpetuals, funding and custody
- 1.2.7 Fixed income and interest rates; CFDs and leveraged products
1.3 · Market data sources
- 1.3.1 Types of market data: EOD, bars, ticks, order book and reference
- 1.3.2 Free market data sources
- 1.3.3 Retail and prosumer market data APIs
- 1.3.4 Professional market data providers
- 1.3.5 Crypto and on-chain data
- 1.3.6 Brokers as market data sources
- 1.3.7 Market data licensing and terms of use
- 1.3.8 How to choose a market data provider
1.4 · Financial data engineering
- 1.4.1 Downloading over REST and websockets: pagination, rate limits and retries
- 1.4.2 Data cleaning: missing values, bad ticks, gaps and time zones
- 1.4.3 Adjusting for splits and dividends: adjusted and unadjusted prices
- 1.4.4 Universes without survivorship bias: delistings, mergers and symbol changes
- 1.4.5 Building bars: time, tick, volume, dollar and imbalance
- 1.4.6 Continuous futures series: roll rules, difference adjustment and ratio adjustment
- 1.4.7 Storage: CSV, Parquet, partitioning, DuckDB, TimescaleDB and ArcticDB
- 1.4.8 Reproducible pipeline: download, validation, incremental updates and quality tests
1.5 · Fundamental and macro data
1.6 · Financial exploratory analysis
1.7 · Programmed technical analysis
Why you need a continuous series
A futures contract has an expiration date. The FIX contract expiring in June 2025, FIXM25, starts trading about nine
months earlier, concentrates almost all the trading in its last three and ceases to exist on June 20, settled against
the index close (lesson 1.2.3). Someone who wants exposure to the index for years does not hold one contract: they
move from one to the next before each expiration. That is the roll.
Research needs the same thing in the form of data: a long series, with no breaks, that represents “holding the FIX future”. It does not exist on the exchange; it is manufactured by splicing contracts. Three decisions have to be made, and each data vendor makes them its own way:
Mermaid source
flowchart TB K[Individual contracts] --> R[Roll rule] R --> M[Contract of the day] M --> E[Spliced series] E --> D[Difference adjustment] E --> T[Ratio adjustment] E --> S[Unadjusted]
- Which contracts go in: every expiration or only those of one cycle (in FIX, the quarterly H, M, U and Z).
- When to switch contracts: a fixed number of sessions before expiration, or when the next one starts to have more volume or more open interest.
- What to do with the price jump of the switch: leave it, subtract it (difference adjustment) or divide it out (ratio adjustment).
The data is the one from level 1's futures module: daily settlements, volume and open interest of 31 FIX contracts between 2019 and 2025, their specifications and the index.
import numpy as np
import pandas as pd
c = pd.read_parquet("fix_contratos.parquet")
spec = pd.read_csv("fix_especificaciones.csv").iloc[0]
liq = c.pivot(index="fecha", columns="contrato", values="liquidacion")
vol = c.pivot(index="fecha", columns="contrato", values="volumen")
oi = c.pivot(index="fecha", columns="contrato",
values="interes_abierto")
venc = c.groupby("contrato")["vencimiento"].first().sort_values()
orden = list(venc.index)
sesiones = liq.index
print(len(orden), "contracts,", len(sesiones), "sessions")
print("multiplier:", spec["multiplicador"], "USD per point")
print("live contracts per session:",
liq.notna().sum(axis=1).value_counts().to_dict())31 contracts, 1760 sessions
multiplier: 50 USD per point
live contracts per session: {3: 1706, 4: 45, 2: 9}Three wide tables, one column per contract and one row per session, with NaN when the contract does not trade.
There are almost always three live contracts; around each expiration, two or four. A snapshot of one day shows how
they are distributed:
indice = pd.read_csv("fix_indice_diario.csv", parse_dates=["fecha"])
indice = indice.set_index("fecha")["indice"]
dia = c[c["fecha"] == "2025-06-02"].set_index("contrato")
dia["base"] = dia["liquidacion"] - indice["2025-06-02"]
print("index:", indice["2025-06-02"])
dia[["vencimiento", "liquidacion", "base", "volumen",
"interes_abierto"]]index: 6359.1
vencimiento liquidacion base volumen interes_abierto
contrato
FIXM25 2025-06-20 6367.75 8.65 315854 786283
FIXU25 2025-09-19 6419.00 59.90 4406 24548
FIXZ25 2025-12-19 6462.50 103.40 729 3201The front contract, FIXM25, takes almost all the volume and open interest; the other two barely trade. Each more
distant expiration trades more expensive: the basis (future minus index) is 8.65 points at 18 days and 103.40 at
a little over six months. It is contango, because the interest rate exceeds the index's dividend yield (the
carry of lesson 1.2.3). That staircase of prices is what produces the jumps when splicing.
Before coding roll rules you need to get the expirations right. The specification says “third Friday of the contract
month; if there is no session, the previous one”, and it is checked against the real NYSE calendar from
exchange_calendars (lesson 1.1.2):
import exchange_calendars as xcals
nyse = xcals.get_calendar("XNYS")
def tercer_viernes(v):
viernes = pd.date_range(v.replace(day=1), periods=31,
freq="D")
viernes = viernes[(viernes.dayofweek == 4)
& (viernes.month == v.month)]
return viernes[2]
esperado = venc.map(lambda v: nyse.date_to_session(
tercer_viernes(v), direction="previous"))
print("expirations per the rule:", (esperado == venc).all())
venc[venc.dt.dayofweek != 4]expirations per the rule: True
contrato
FIXM26 2026-06-18
Name: vencimiento, dtype: datetime64[us]All 31 expirations follow the rule, and only one does not fall on a Friday: FIXM26, on June 18, 2026, because
Friday the 19th is Juneteenth and the NYSE is closed. A “hand-made” calendar that only looked at the day of the week
would get it wrong there.
When to switch contracts
Mermaid source
flowchart TB
S[Session close] --> C{Crossover yesterday?}
C -- yes --> R[Roll at the close]
C -- no --> N{Expires in 2 days?}
N -- yes --> R
N -- no --> M[Hold]The three usual rules:
- Calendar. Switch a fixed number of sessions before expiration. It is predictable and needs no data: it is known months in advance. Its risk is switching too early or too late if liquidity moves on another day.
- Volume. Switch when the next contract trades more than the front one. It follows liquidity, which is where execution is best.
- Open interest. The same with the number of open contracts, which changes more slowly and with less noise than volume.
The diagram combines the two ideas: roll on crossover and, if it does not come, a calendar safety net so you never reach expiration with the position open. The next two functions implement each rule separately. The important detail is in the second one: a session's volume is only known when it ends, so the crossover observed at the close of day is executed at the close of .
def roll_calendario(n):
"""Switch at the close of the session n before expiration."""
return {a: sesiones[sesiones.get_loc(venc[a]) - n]
for a in orden if venc[a] <= sesiones[-1]}
def roll_cruce(tabla, solo_frontal=True):
"""Session after the first one in which the next contract
exceeds the current one: the day's volume is known at the close."""
rolls = {}
for i, (a, b) in enumerate(zip(orden, orden[1:])):
if venc[a] > sesiones[-1]:
break
par = tabla[[a, b]].dropna()
par = par[par.index <= venc[a]]
if solo_frontal and i > 0: # a is already the front contract
par = par[par.index > venc[orden[i - 1]]]
cruces = par.index[par[b] > par[a]]
rolls[a] = sesiones[sesiones.get_loc(cruces[0]) + 1]
return rolls
def sesiones_antes(rolls):
return pd.Series({a: sesiones.get_loc(venc[a])
- sesiones.get_loc(d) for a, d in rolls.items()})
reglas = {"calendario_5": roll_calendario(5),
"volumen": roll_cruce(vol),
"interes_abierto": roll_cruce(oi)}
antes = pd.DataFrame({k: sesiones_antes(v) for k, v in reglas.items()})
antes.apply(pd.Series.value_counts).fillna(0).astype(int) calendario_5 volumen interes_abierto
3 0 14 28
4 0 14 0
5 28 0 0The table counts, for the 28 rolls from 2019 to 2025, how many sessions before expiration each rule switches. By volume, three or four (14 times each); by open interest, always three; the calendar, five by construction. In FIX liquidity moves to the next contract in the last week and a half, as the data README describes, and the three rules fall within one or two sessions of each other. In real markets the difference can be larger: there are futures whose roll starts weeks earlier, and deliverable ones (bonds, commodities) where you have to get out before the first notice day to avoid taking delivery.
The solo_frontal parameter prevents a mistake that does not show in the code but does in the results. If the
crossover is searched over the contract's whole life, and not only while it is the front one, something like this
appears:
mal = sesiones_antes(roll_cruce(vol, solo_frontal=False))
print("sessions before expiration:", mal.value_counts().to_dict())sessions before expiration: {3: 8, 4: 6, 126: 6, 124: 4, 127: 2, 123: 1, 125: 1}In 14 of the 28 rolls, the “crossover” comes out between 123 and 127 sessions before expiration: half a year. When a contract is still the second on the curve, its volume and that of the third are small and noisy, and the third exceeds it on some day by chance. That day means nothing: liquidity is in another contract. The comparison only makes sense between the front contract and the next one.
With the roll dates, each session has its contract. The switch happens at the close of the roll session: that day the position ends in the new contract.
def mantenido(rolls):
"""Contract in the portfolio at the close of each session: the
roll is done at the close of the crossover session."""
out, k = [], 0
for t in sesiones:
while orden[k] in rolls and t >= rolls[orden[k]]:
k += 1
out.append(orden[k])
return pd.Series(out, index=sesiones)
rolls = reglas["volumen"]
h = mantenido(rolls)
h["2025-06-10":"2025-06-16"]fecha
2025-06-10 FIXM25
2025-06-11 FIXM25
2025-06-12 FIXM25
2025-06-13 FIXU25
2025-06-16 FIXU25
dtype: strThe volume of FIXU25 exceeds that of FIXM25 on June 12, 2025 (341,427 contracts versus 330,646); the roll is done
at the close of the 13th. The figure shows open interest around those dates: that of the June contract goes from
about 780,000 contracts to 44,111 on expiration day, and that of September rises above 800,000. The two curves cross
between June 12 and 13.
Splicing: the unadjusted series and its gaps
The simplest form of continuous series is to take, each session, the price of the contract held at the close. It works until roll day: that day the series jumps from the old contract's price to the new one's, and the jump is nobody's gain or loss. It is the difference between two different contracts at the same instant, the roll gap:
precio_h = pd.Series([liq.at[t, k] for t, k in h.items()],
index=sesiones)
huecos = pd.DataFrame([
{"fecha": d, "viejo": liq.at[d, a],
"nuevo": liq.at[d, orden[orden.index(a) + 1]]}
for a, d in rolls.items()]).set_index("fecha")
huecos["hueco"] = huecos["nuevo"] - huecos["viejo"]
huecos["ratio"] = huecos["nuevo"] / huecos["viejo"]
print(f"{len(huecos)} rolls; gaps from {huecos['hueco'].min()} "
f"to {huecos['hueco'].max()} points; "
f"{(huecos['hueco'] < 0).sum()} negative")
huecos.loc["2020-03":"2020-12"].round(4)28 rolls; gaps from -17.5 to 53.5 points; 10 negative
viejo nuevo hueco ratio
fecha
2020-03-16 2553.25 2542.75 -10.50 0.9959
2020-06-16 3050.25 3038.00 -12.25 0.9960
2020-09-14 3211.75 3200.00 -11.75 0.9963
2020-12-14 3831.50 3815.00 -16.50 0.9957The 28 gaps range from −17.5 to 53.5 points. Their sign tells the story of interest rates in the data: from December 2019 to March 2022, with rates near zero and below the dividend yield, the next contract traded cheaper than the front one (backwardation) and the ten negative gaps are there. From June 2022, with high rates, contango and gaps of 30 to 50 points. In dollars, a gap of 53.5 points is 2,675 USD per contract (× 50): a system reading the unadjusted series would see that “gain” every quarter without anybody collecting it.
Adjusting: by difference or by ratio
There are two ways to remove the jumps, and both rewrite the whole history before each roll so it fits the new contract:
- By difference (back-adjusted, additive): the gap is added to every price before the roll.
- By ratio (proportional): every earlier price is multiplied by the new / old quotient.
In both, the last stretch (the contract live today) keeps its real prices and everything before it shifts. That is why the adjusted series changes backwards every time a roll is added: a 2019 price in today's series is not the same as in the series of a year ago.
dif = precio_h.copy()
rat = precio_h.copy()
for d, fila in huecos.iterrows():
antes_del_roll = dif.index < d
dif[antes_del_roll] += fila["hueco"]
rat[antes_del_roll] *= fila["ratio"]
series = pd.DataFrame({"sin_ajustar": precio_h, "diferencia": dif,
"ratio": rat})
print(series.iloc[[0, -1]].round(2))
print("sum of gaps:", huecos["hueco"].sum(), "points") sin_ajustar diferencia ratio
fecha
2019-01-02 2536.50 2981.50 2748.05
2025-12-31 7027.75 7027.75 7027.75
sum of gaps: 445.0 pointsThe three series end at 7027.75, the real price of the live contract at the end of 2025, and start in three different places: 2536.50 the unadjusted one (what was actually traded on January 2, 2019), 2981.50 the difference-adjusted one (2536.50 plus the 445 points of accumulated gaps) and 2748.05 the ratio-adjusted one. The figure at the top plots the three and, below, the distance of the other two from the ratio one: up to 9.5% for the difference one and −10.6% for the unadjusted one.
Which of the three series tells the truth
The test is to compare each series with what a one-contract position actually earns. Each session, the position is the contract held at the previous close, and it earns that contract's change between the two closes. Roll day is no exception: until the close, you hold the old one.
# what the position actually earns: the contract held at the
# previous close, from one close to the next
antes_h = h.shift(1).iloc[1:]
p0 = pd.Series([liq.at[t, k] for t, k in
zip(sesiones[:-1], antes_h)], index=sesiones[1:])
p1 = pd.Series([liq.at[t, k] for t, k in antes_h.items()],
index=sesiones[1:])
real_pts, real_pct = p1 - p0, p1 / p0 - 1
for nombre, s in series.items():
err_pts = (s.diff().iloc[1:] - real_pts).abs().max()
err_pct = (s.pct_change().iloc[1:] - real_pct).abs().max()
print(f"{nombre:12s} max error: {err_pts:6.2f} points, "
f"{100 * err_pct:.3f} pp")sin_ajustar max error: 53.50 points, 1.039 pp
diferencia max error: 0.00 points, 1.084 pp
ratio max error: 40.36 points, 0.000 ppEach series gets one thing right at most:
| Series | Change in points | % return | Price level | Use |
|---|---|---|---|---|
| Unadjusted | False at the rolls (up to 53.5) | False at the rolls (1.04 pp) | Real | Charts of traded prices, levels |
| By difference | Exact | False (up to 1.08 pp) | Shifted | Results in points and dollars |
| By ratio | False (up to 40.36) | Exact | Shifted | Returns, volatility, % stops |
The percentage error of the difference series is not concentrated at the rolls: it is in every old session, because it divides real changes by a level that is not the real one. A 2019 session with a change of 30 points on a real price of 2536.50 is 1.18%; on the shifted level of 2981.50, 1.01%. With the ratio one the opposite happens: the percentage is exact and the points are not.
Negative prices and the cost of holding the future
In FIX the gaps partly offset each other (negative from 2019 to 2022, positive afterwards) and the difference-adjusted series does not fall below 2,700. In markets with persistent contango (commodities with storage costs, volatility futures in calm periods) the gaps are almost always of the same sign and accumulate for years. A sketch shows where that leads:
# sketch: the spot price does not move (20 USD) and each monthly
# contract trades 4% above the previous one; 10 years of rolls
contado, contango, meses = 20.0, 0.04, 120
hueco_mes = contado * contango
print("difference adjustment, first price:",
round(contado - hueco_mes * meses, 2))
print("ratio adjustment, first price:",
round(contado / (1 + contango) ** meses, 4))difference adjustment, first price: -76.0
ratio adjustment, first price: 0.1807A hundred and twenty gaps of 0.80 USD add up to 96 USD, and the first difference-adjusted price comes out at −76. With a negative price you cannot compute a return or a logarithm, and any indicator that divides by the price breaks. CSI Data's option for this case is to add a fixed amount to the whole series so there are no negatives, which fixes the symptom and shifts the level even further. Ratio adjustment does not produce negatives (0.18), but a price of 18 cents in a series of an asset that has always traded at 20 USD distorts any level-based rule just as much.
What really matters in a continuous series is its returns, and the ratio one has them exact. Compared with those of the index, they show the cost of holding the future:
curva = pd.read_csv("curva_tipos_diaria.csv", parse_dates=["fecha"])
y = curva.set_index("fecha")["3m"]
anios = range(2019, 2026)
fut = rat.groupby(rat.index.year).last()
fut = fut / fut.shift(1).fillna(rat.iloc[0]) - 1
ind = indice.groupby(indice.index.year).last()
ind = ind / ind.shift(1).fillna(indice.iloc[0]) - 1
tabla = pd.DataFrame({
"indice_pct": 100 * ind, "futuro_pct": 100 * fut,
"diferencia": 100 * (fut - ind),
"tipo_3m": y.groupby(y.index.year).mean(),
"dividendo": [1.9 - 0.1 * (a - 2019) for a in anios],
}, index=anios)
tabla["div_menos_tipo"] = tabla["dividendo"] - tabla["tipo_3m"]
tabla.round(2) indice_pct futuro_pct diferencia tipo_3m dividendo div_menos_tipo
2019 30.58 29.99 -0.59 2.10 1.9 -0.20
2020 14.42 15.90 1.48 0.33 1.8 1.47
2021 11.40 13.29 1.89 0.20 1.7 1.50
2022 -13.52 -13.69 -0.17 2.37 1.6 -0.77
2023 23.09 18.98 -4.10 5.04 1.5 -3.54
2024 36.32 31.28 -5.04 4.94 1.4 -3.54
2025 14.30 11.13 -3.17 4.05 1.3 -2.752019 starts on January 2, the first session in the data. The dividendo column approximates by year the index's
dividend yield, which according to the data README falls linearly from 1.9% in 2019 to 1.3% in 2025.
The future does not return the same as the price index. A futures long is equivalent to holding the index financed with debt: it does not collect dividends and does not pay interest on the money it has not put up. Its return relative to the price index resembles dividend − rate: +1.48 points in 2020 versus +1.47, +1.89 in 2021 versus +1.50, and −4.10, −5.04 and −3.17 from 2023 to 2025 versus −3.54, −3.54 and −2.75. They do not match exactly because the cost is applied on a notional that changes during the year (in 2024 the index rises 36.32%) and because the rates and the dividend in the table are averages. That carry cost or income is what the jargon calls roll yield: in contango, each roll sells the converging contract cheap and buys the next one expensive.
What this changes in your data
- Ask which series you are given. “Continuous” is not enough: roll rule, adjustment method and date of the last adjustment. Two continuous series of the same future from two vendors do not have the same old prices.
- The adjusted history is rewritten with each roll. If you store an adjusted series and download it again months later, the old prices will have changed. Store the individual contracts and build the series yourself (lesson 1.4.7).
- Returns with ratio, dollars with difference, levels with the contracts. And never the unadjusted series to compute returns.
- The roll is decided with data already published. Today's volume is for tomorrow; open interest, depending on the source, sometimes arrives even later.
Exercises
- Compare the calendar rule (five sessions before) with the volume rule: in how many sessions is a different contract held? Why does the 2019 to 2025 result barely change?
Solution
h_cal = mantenido(reglas["calendario_5"])
distintos = (h_cal != h).sum()
print(f"Sessions with a different contract: {distintos}")Sessions with a different contract: 42That is 42 sessions out of 1,760: one or two per roll, the ones that separate the calendar switch (five sessions before) from the volume switch (three or four). In those sessions you hold the next contract instead of the front one, and the two move almost the same: the difference in result is only that of the basis between them, which changes little over a couple of sessions. Where the rules do matter is in markets with a slow roll or physical delivery, and in execution: switching when the new contract does not yet have liquidity costs more bid-ask spread.
- Compute the result in points and in dollars of holding two long FIX contracts during 2022 and check that it matches that of the difference-adjusted series.
Solution
pts_2022 = real_pts["2022"].sum()
print(f"Points gained in 2022: {pts_2022:.2f}")
print(f"Result of 2 contracts: "
f"{2 * spec['multiplicador'] * pts_2022:,.0f} USD")
print("with the difference-adjusted series:",
round(dif["2022"].iloc[-1] - dif[:"2021"].iloc[-1], 2))Points gained in 2022: -573.50
Result of 2 contracts: -57,350 USD
with the difference-adjusted series: -573.5Two long contracts lose 573.50 points in 2022, 57,350 USD with a multiplier of 50. The difference-adjusted series gives exactly the same points between the last close of 2021 and that of 2022, because it preserves all the daily changes. With the unadjusted series you would get another figure: the four 2022 gaps would be added as if they were gains or losses.
- A colleague computes the annualized volatility of the FIX future with the unadjusted spliced series and another with the difference-adjusted one. What mistake does each one make and which series would you use?
Solution
The first one puts into the volatility 28 false jumps, one per roll, of up to 53.5 points (more than 1% in 2024): days of “return” that nobody earned and that inflate volatility and tails. The second one has no jumps, but computes returns by dividing real changes by shifted levels: in the old years, with a level higher than the real one (up to 9.5% in 2020), it underestimates the returns and therefore the volatility of those years. The right series for any percentage measure is the ratio-adjusted one, whose returns match exactly those of holding the contracts.
Each answer is folded under its question
Self-assessment
In the difference-adjusted FIX series, which measure matches exactly that of actually holding the contracts?
- AThe percentage return of each session
- BThe 2019 price level
- CNone
- DThe change in points of each session, and therefore the result in dollars
Show answer
Answer D. The change in points of each session, and therefore the result in dollars
Difference adjustment adds each roll's gap to the whole earlier history, so the differences between sessions do not change: the maximum error in points is 0.00. But the level does change (2981.50 versus 2536.50 in 2019) and with it the percentage returns, which end up off by as much as 1.08 percentage points.
A system uses 2% stops and log returns. Which continuous series suits it?
- AThe unadjusted spliced one
- BThe ratio-adjusted one
- CThe difference-adjusted one
- DIt does not matter
Show answer
Answer B. The ratio-adjusted one
Ratio adjustment multiplies the earlier history by the new/old quotient of each roll, and so it preserves percentage returns exactly (error 0.000 pp in FIX). In exchange, the points of each session are no longer the real ones, and to compute results in dollars you have to go back to the contracts or to the difference series.
The lesson's roll rule switches contract at the close of the session after the volume crossover, not at the close of the crossover day itself. Why?
- ABecause the exchange does not allow trading on the crossover day
- BBecause that way the gaps are smaller
- CBecause a session's volume is only known when it ends: deciding with it at its own close would use a figure that did not exist yet
- DOut of vendor habit
Show answer
Answer C. Because a session's volume is only known when it ends: deciding with it at its own close would use a figure that did not exist yet
It is the same idea as in any signal: a figure from day t is used to act after t. The volume crossover from FIXM25 to FIXU25 was on June 12, 2025; the roll is done at the close of the 13th.
Why can difference adjustment produce negative prices?
- ABecause it adds to or subtracts from the old history the sum of all later gaps: in a market in strong contango for years, that sum can exceed the old price
- BBecause of a rounding error
- CBecause futures can trade negative
- DBecause the ratio can be negative
Show answer
Answer A. Because it adds to or subtracts from the old history the sum of all later gaps: in a market in strong contango for years, that sum can exceed the old price
In the lesson's sketch, a flat spot price of 20 USD with 4% monthly contango accumulates 96 USD of gaps over 120 rolls, and the first adjusted price comes out at −76. Ratio adjustment gives 0.18: positive, although just as far from what was traded.
From 2023 to 2025 the FIX future returns between 3.2 and 5.0 points less a year than its price index. Where does that difference come from?
- AFrom an error in the data
- BFrom carry: with rates above the dividend yield the future trades above the index and converges to it, so the futures long pays the difference at each roll
- CFrom roll commissions
- DFrom the fact that the future expires
Show answer
Answer B. From carry: with rates above the dividend yield the future trades above the index and converges to it, so the futures long pays the difference at each roll
In those years the 3-month rate (between 4.05% and 5.04% on average) exceeds the dividend yield (1.3% to 1.5%): contango. The future's price falls toward the index as expiration approaches, and that drop is a cost for the long. From 2020 to 2021, with rates near zero, the opposite happened and the future returned more.
Key points
- A continuous series is a chain of contracts plus a roll rule and an adjustment method; each vendor picks its own and you need to know which.
- A volume or open interest roll is decided with closing data and executed afterwards; in FIX it falls three or four sessions before expiration.
- The unadjusted spliced series has false jumps at each roll; the difference-adjusted one preserves points (and the dollar result) and the ratio-adjusted one preserves percentage returns.
- No adjusted series shows the prices that were traded: for levels, price stops and comparisons with the index, use the real contracts.
- Difference adjustment can produce negative prices in markets with persistent contango; ratio adjustment cannot, but it distorts old levels just as much.
- In contango, holding the future costs relative to the index (from −3.2 to −5.0 points a year in FIX from 2023 to 2025); in backwardation, it adds.