---
title: "1.4.6 Continuous futures series and roll adjustment"
description: "From an expiring future to a long series: calendar, volume or open interest rolls and difference or ratio adjustment, with 31 FIX contracts."
url: "https://pythonparatrading.com/en/python-trading-course/level-1/continuous-futures-series-and-roll-adjustment/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-1/continuous-futures-series-and-roll-adjustment.png"
---

# 1.4.6 · 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.

- Course: [Level 1: Financial data, markets and analysis](https://pythonparatrading.com/en/python-trading-course/level-1/)
- Module: 1.4 · Financial data engineering
- Duration: 60 min
- Prerequisites: [1.2.3 · Futures: specifications, margin, roll and contango](https://pythonparatrading.com/en/python-trading-course/level-1/futures-contract-specs-margin-and-roll/), [0.5.3 · Selection, filtering, groupby, merge, pivot, resample and moving windows](https://pythonparatrading.com/en/python-trading-course/level-0/pandas-groupby-merge-resample-rolling/)

## By the end of this lesson

- Code three roll rules (days before expiration, volume crossover and open interest crossover) without using information from the future.
- Build the unadjusted spliced series, the difference-adjusted one and the ratio-adjusted one, and measure the gaps at each roll.
- Check what each series preserves: points gained (difference), percentage returns (ratio) or neither (unadjusted).
- Explain the risk of negative prices in difference adjustment and the cost of holding a future in contango.

## 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
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]
```

1. **Which contracts go in**: every expiration or only those of one cycle (in FIX, the quarterly H, M, U and Z).
2. **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.
3. **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.

```python
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())
```
```output
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:

```python
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"]]
```
```output
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             3201
```

The 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):

```python
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]
```
```output
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
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
$t$ is executed at the close of $t+1$.

```python
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)
```
```output
   calendario_5  volumen  interes_abierto
3             0       14               28
4             0       14                0
5            28        0                0
```

The 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.

<aside class="callout" data-kind="nota">

Two vendors, two conventions. Norgate Data publishes its continuous series unadjusted and difference-adjusted, and
rolls on the session before the last trading day for cash-settled contracts and the one before the first notice day
for deliverable ones. CSI Data lets you choose a roll by volume and/or open interest, with one or with several
confirmation sessions. Before comparing your own backtest with one built on a vendor's data, you need to know which
rule it uses.

</aside>

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:

```python
mal = sesiones_antes(roll_cruce(vol, solo_frontal=False))
print("sessions before expiration:", mal.value_counts().to_dict())
```
```output
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.

```python
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"]
```
```output
fecha
2025-06-10    FIXM25
2025-06-11    FIXM25
2025-06-12    FIXM25
2025-06-13    FIXU25
2025-06-16    FIXU25
dtype: str
```

*Fig. — FIX roll from June to September 2025: open interest of each contract, the volume roll (close of 13 June) and expiry (20 June).* Two open-interest lines between 27 May and 24 June 2025. FIXM25 stays around 780,000 contracts until 5 June and falls to 44,111 on expiry day; FIXU25 rises from about 25,000 to over 800,000. The lines cross between 12 and 13 June. A highlighted vertical line marks the roll at the close of 13 June, the session after the volume crossover, and another the 20 June expiry.

The 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**:

```python
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)
```
```output
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.9957
```

*Fig. — Gap at each FIX futures roll, 2019–2025: new contract price minus old contract price at the close of the roll day, in points.* 28 vertical bars, one per roll. Small and positive in 2019 (up to 4.75 points); negative (backwardation) from December 2019 to March 2022, from −1.75 to −17.5 points, when the dividend yield exceeded near-zero rates; positive (contango) from June 2022, peaking at 53.5 points in March and June 2024 (highlighted).

The 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.

```python
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")
```
```output
            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 points
```

The 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.

```python
# 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")
```
```output
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 pp
```

Each 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.

<aside class="callout" data-kind="riesgo">

No adjusted series contains the prices that were traded. A stop “at 3,000 points” in 2019, a comparison with the
index level or a limit order in a backtest have to use the price of that day's real contract, not that of a series
that has been shifted backwards.

</aside>

## 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:

```python
# 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))
```
```output
difference adjustment, first price: -76.0
ratio adjustment, first price: 0.1807
```

A 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:

```python
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)
```
```output
      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.75
```

2019 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.

<aside class="callout" data-kind="aviso">

If a backtest compares a futures system against the price index, the comparison is biased by carry: in favor of the
future with low rates, against it with high rates. The fair comparison is against the total return index minus the
interest rate, or directly against holding the continuous future.

</aside>

## 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

1. 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.**

```python
h_cal = mantenido(reglas["calendario_5"])
distintos = (h_cal != h).sum()
print(f"Sessions with a different contract: {distintos}")
```
```output
Sessions with a different contract: 42
```

That 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.

2. 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.**

```python
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))
```
```output
Points gained in 2022: -573.50
Result of 2 contracts: -57,350 USD
with the difference-adjusted series: -573.5
```

Two 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.

3. 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.

## Self-assessment

1. In the difference-adjusted FIX series, which measure matches exactly that of actually holding the contracts?

   - A. The percentage return of each session
   - B. The 2019 price level
   - C. None
   - D. The change in points of each session, and therefore the result in dollars

   *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.

2. A system uses 2% stops and log returns. Which continuous series suits it?

   - A. The unadjusted spliced one
   - B. The ratio-adjusted one
   - C. The difference-adjusted one
   - D. It does not matter

   *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.

3. 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?

   - A. Because the exchange does not allow trading on the crossover day
   - B. Because that way the gaps are smaller
   - 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
   - D. Out of vendor habit

   *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.

4. Why can difference adjustment produce negative prices?

   - 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
   - B. Because of a rounding error
   - C. Because futures can trade negative
   - D. Because the ratio can be negative

   *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.

5. 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?

   - A. From an error in the data
   - 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
   - C. From roll commissions
   - D. From the fact that the future expires

   *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.

## Resources

### Research

- [Seasonality in Python. Searching for Intraday Patterns](https://pythonparatrading.com/en/seasonality-intraday-preprocessing/) · Research-0024 · 9 min

### Documentation

- [Norgate Data: futures package (unadjusted and difference-adjusted continuous series; roll date)](https://norgatedata.com/futurespackage.php) · norgatedata.com
- [CSI Data: continuous series adjustment options (volume or open interest roll, confirmation, non-negative prices)](https://www.csidata.com/custserv/onlinehelp/OnlineManual/back_adjustingsettings.htm) · csidata.com
- [exchange_calendars: market session calendars in Python](https://github.com/gerrymanoim/exchange_calendars) · github.com

### Books

- [Marcos López de Prado: Advances in Financial Machine Learning (Wiley, 2018), section 2.4.3, “Single Future Roll”](https://www.wiley.com/en-us/Advances+in+Financial+Machine+Learning-p-9781119482086) · wiley.com

### Data

- [Level 1 data: FIX futures contracts, 2019-2025 (Parquet)](https://pythonparatrading.com/curso/datos/nivel-1/fix_contratos.parquet) · Download · fix_contratos.parquet
- [Level 1 data: daily FIX index (CSV)](https://pythonparatrading.com/curso/datos/nivel-1/fix_indice_diario.csv) · Download · fix_indice_diario.csv

Previous lesson: [1.4.5 · Building bars: time, tick, volume, dollar and imbalance](https://pythonparatrading.com/en/python-trading-course/level-1/time-tick-volume-and-dollar-bars/)

Next lesson: [1.4.7 · Storage: CSV, Parquet, partitioning, DuckDB, TimescaleDB and ArcticDB](https://pythonparatrading.com/en/python-trading-course/level-1/storing-market-data-parquet-duckdb-timeseries/)
