---
title: "3.1.2 Futures curve: basis, contango and roll yield"
description: "Cost of carry in index, bond, currency and commodity futures; contango, backwardation, convenience yield and roll yield, measured."
url: "https://pythonparatrading.com/en/python-trading-course/level-3/futures-curve-basis-contango-roll-yield/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-3/futures-curve-basis-contango-roll-yield.png"
---

# 3.1.2 · Futures curve: basis, cost of carry, contango and roll yield

> Several expiries of the same future trade every day, and the shape of that curve is not a market forecast: it is, above all, the cost of holding the underlying until each date. This lesson measures that cost in the four kinds of future (indices, bonds, currencies and commodities), splits what someone holding a future earns into price change and roll yield, and checks with the known answer of the synthetic data when roll yield is a gain and when it is not.

- Course: [Level 3: Derivatives: futures, spreads, forex and options](https://pythonparatrading.com/en/python-trading-course/level-3/)
- Module: 3.1 · Futures in depth
- Duration: 65 min
- Prerequisites: [3.1.1 · Main futures contracts](https://pythonparatrading.com/en/python-trading-course/level-3/main-futures-contracts/), [1.2.3 · Futures: specifications, margin, roll and contango](https://pythonparatrading.com/en/python-trading-course/level-1/futures-contract-specs-margin-and-roll/), [1.4.6 · Continuous futures series: roll rules, difference adjustment and ratio adjustment](https://pythonparatrading.com/en/python-trading-course/level-1/continuous-futures-series-and-roll-adjustment/), [2.3.4 · Carry in currencies, futures and bonds; seasonality and calendar effects](https://pythonparatrading.com/en/python-trading-course/level-2/carry-seasonality-calendar-effects/)

## By the end of this lesson

- Write the cost of carry of a future on an index, a bond, a currency and a commodity, and calculate its fair value and its basis.
- Extract from the curve what the market is pricing in: the implied dividend of an index and the net convenience yield of a commodity.
- Measure contango and backwardation in 23 markets and distinguish the slope that is carry from the seasonal shape of the curve.
- Decompose the return of holding a future into price change plus roll yield, and relate roll yield to the average carry.
- Recognize the Samuelson effect (far expiries move less) and its consequences for choosing the contract.

## One curve, many prices

Lesson 1.2.3 introduced basis and contango with a single index. Here it is generalized to the four kinds of future.
The idea is the same in all of them: buying the future is equivalent to buying the underlying today and holding it
until expiry, with what that costs (financing, storage) and what it yields (dividends, coupons, interest on the other
currency, the usefulness of having the commodity). The difference between the future and spot is that **cost of
carry**:

$$
F(t, T) = S_t \, e^{(r + u - y)\,\tau}, \qquad \tau = T - t
$$

with $r$ the financing rate, $u$ the storage cost and $y$ everything that holding the underlying yields: the dividend
of an index, the interest rate of the foreign currency, or the **convenience yield** of a commodity (what it is worth
to have the barrel in the tank instead of a piece of paper).

```mermaid
flowchart TB
  S[Spot] --> R[+ financing r]
  R --> U[+ storage u]
  U --> Y[− what it yields y]
  Y --> F[Future τ years out]
  Y --> Q[Index: dividend]
  Y --> D[Currency: its rate]
  Y --> C[Convenience yield]
```

When the far future is above the near one, the curve is in **contango** ($r + u > y$); below, in **backwardation**. In
financials, all the terms are observed and arbitrage sets the curve. In commodities, the convenience yield is not
observed: it is inferred from the curve, and it changes with scarcity.

## Indices: the basis is the cost of carry

For each index, the data has the spot, the dividend yield and the one-month rate of its currency:

```python
import numpy as np
import pandas as pd

curvas = pd.read_parquet("futuros3_curvas.parquet")
ctr = pd.read_csv("futuros3_contratos.csv",
                  parse_dates=["vencimiento", "primer_aviso",
                               "fecha_alta", "fecha_roll"])
esp = pd.read_csv("futuros3_especificaciones.csv",
                  index_col="mercado")
contado = pd.read_csv("futuros3_indices_contado.csv",
                      parse_dates=["fecha"])
venc = ctr.set_index("contrato")["vencimiento"]
curvas["dias"] = (venc.reindex(curvas["contrato"]).to_numpy()
                  - curvas["fecha"]).dt.days
DIA = "2025-11-26"
c = contado[(contado["fecha"] == DIA)].set_index("indice")
c[["contado", "rentabilidad_dividendo", "tipo_1m"]]
```
```output
         contado  rentabilidad_dividendo  tipo_1m
indice
RVA      7841.33                   1.891     3.87
RVB     26175.84                   0.959     3.87
RVC      3226.71                   1.227     3.87
RVE      6950.49                   2.947     2.09
RVH     19718.00                   3.570     2.09
```

On November 26, 2025, the dollar indices (`RVA`, `RVB`, `RVC`) are financed at 3.87% and those of the evro zone
(`RVE`, `RVH`), at 2.09%. The dividend goes from 0.959% for `RVB` (a technology index, like the Nasdaq-100) to 3.570%
for `RVH`. With those three numbers, the whole curve is determined:

```python
def curva(mercado, dia):
    d = curvas[(curvas["mercado"] == mercado)
               & (curvas["fecha"] == dia)]
    return d.sort_values("dias").set_index("contrato")[
        ["dias", "liquidacion"]]

for m in ["RVA", "RVE"]:
    S, q, r = c.loc[m, ["contado", "rentabilidad_dividendo",
                        "tipo_1m"]]
    d = curva(m, DIA)
    tau = d["dias"] / 365
    d["razonable"] = S * np.exp((r - q) / 100 * tau)
    d["base"] = d["liquidacion"] - S
    d["dif_pb"] = 1e4 * np.log(d["liquidacion"] / d["razonable"])
    print(m, f"S = {S}, r − q = {r - q:.2f} %")
    print(d.round(2).to_string(), "\n")
```
```output
RVA S = 7841.33, r − q = 1.98 %
          dias  liquidacion  razonable    base  dif_pb
contrato
RVAZ25      23      7851.75    7851.11   10.42    0.81
RVAH26     114      7892.00    7889.95   50.67    2.60
RVAM26     204      7930.50    7928.54   89.17    2.47
RVAU26     296      7970.25    7968.19  128.92    2.59
RVAZ26     387      8009.75    8007.60  168.42    2.68

RVE S = 6950.49, r − q = -0.86 %
          dias  liquidacion  razonable   base  dif_pb
contrato
RVEZ25      23       6946.0    6946.74  -4.49   -1.06
RVEH26     114       6929.0    6931.91 -21.49   -4.20
RVEM26     204       6914.0    6917.28 -36.49   -4.74
RVEU26     296       6900.0    6902.35 -50.49   -3.41
RVEZ26     387       6885.0    6887.62 -65.49   -3.80
```

In `RVA`, with the rate above the dividend ($r - q = 1.98$%), the curve is in contango: the basis grows from 10.42
points at 23 days to 168.42 at 387. In `RVE`, with the dividend above the rate ($r - q = -0.86$%), in backwardation:
from −4.49 to −65.49. The ten contracts are within 5 basis points of their fair value (`dif_pb`, between −4.74 and
+2.68): the difference is the rounding to the tick and a small noise that the generator adds and dampens as expiry
approaches. In real markets, the basis also carries the uncertainty of future dividends and the real cost of financing,
which is not the one-month rate.

Put the other way round: if the future and the rate are observed, the dividend the market is pricing in comes out of
the curve:

$$
q_{\text{impl}} = r - \frac{\ln(F/S)}{\tau}
$$

```python
def que_se_tiene(fechas, mercado):
    cm = ctr[ctr["mercado"] == mercado].reset_index(drop=True)
    pos = np.searchsorted(cm["fecha_roll"].to_numpy(),
                          fechas.to_numpy(), side="right")
    return cm["contrato"].to_numpy()[pos]

filas = {}
for m in ["RVA", "RVB", "RVC", "RVE", "RVH"]:
    s = contado[contado["indice"] == m].set_index("fecha")
    tiene = que_se_tiene(s.index, m)
    f = curvas.set_index(["fecha", "contrato"])
    f = f.loc[list(zip(s.index, tiene))]
    tau = f["dias"].to_numpy() / 365
    F = f["liquidacion"].to_numpy()
    q_imp = 100 * (s["tipo_1m"] / 100
                   - np.log(F / s["contado"]) / tau)
    lejos = tau > 30 / 365            # with small τ, the tick dominates
    err = (q_imp - s["rentabilidad_dividendo"])[lejos]
    filas[m] = {"q_medio": s["rentabilidad_dividendo"].mean(),
                "error_medio": err.mean(), "error_dt": err.std()}
pd.DataFrame(filas).T.round(3)
```
```output
     q_medio  error_medio  error_dt
RVA    1.855       -0.003     0.115
RVB    0.846        0.019     0.114
RVC    1.186        0.010     0.118
RVE    2.667        0.007     0.128
RVH    4.003        0.009     0.125
```

The implied dividend recovers the true one with an average error of hundredths of a point and a standard deviation of
0.11 to 0.13 percentage points, over every session from 2010 to 2025 in which the contract has more than 30 days left.
It is the known answer: the generator builds each future as the fair value plus a noise of about 2 basis points. With
less than a month to expiry, a one-tick difference is divided by a very small τ and the error blows up; hence the
filter. Dividend traders do this same thing with real futures, the other way round: they estimate the dividends and
look for mispriced futures.

## Bonds and currencies: coupon, financing and rates

In a currency future, what the underlying yields is the interest rate of the foreign currency: the curve is covered
interest parity (lesson 1.2.4). In a bond future, it is the coupon against the financing of the bond:

```python
for m in ["TPC", "DQE"]:
    print(m, curva(m, DIA)["liquidacion"].round(5).to_dict())
tipos = pd.read_csv("tipos3_curva.csv", index_col="fecha")
print(tipos.loc[DIA, ["usd_1m", "qev_1m", "usd_10a"]].to_dict())
```
```output
TPC {'TPCZ25': 113.04688, 'TPCH26': 113.07812, 'TPCM26': 113.125}
DQE {'DQEZ25': 1.36395, 'DQEH26': 1.37, 'DQEM26': 1.3761, 'DQEU26': 1.3822, 'DQEZ26': 1.38835}
{'usd_1m': 3.87, 'qev_1m': 2.09, 'usd_10a': 3.939}
```

The evro future (`DQE`) rises with maturity: from 1.36395 to 1.38835 in one year, about 1.8%, the difference between
the dollar's 3.87% and the evro's 2.09%. Whoever buys evros forward does not collect the evro rate and does not pay the
dollar rate, and the curve compensates for it.

The 10-year note is subtler. The delivered bond pays a 6% coupon and is financed at 3.87%: the carry is positive, and
that pushes the curve down (backwardation). But in this data, as in the real contracts, each expiry delivers a
different bond: the standard bond of `TPC` matures 6.6 years after the expiry of the future, so the March contract
delivers a bond three months longer than December's. With a coupon above the yield, a longer bond is worth more, and
that pushes the curve up:

```python
def precio_bono(y, anios, cupon=6.0):
    """Clean price of a bond with an annual coupon maturing in
    `anios` (may be fractional), yield y, per 100 of face value."""
    t = np.arange(anios, 0, -1.0)[::-1]          # cash flows
    sucio = (cupon * (1 + y) ** -t).sum() + 100 * (1 + y) ** -anios
    return sucio - cupon * (1 - anios % 1)   # without accrued coupon

y, r = 0.0375, tipos.loc[DIA, "usd_1m"] / 100
P1, P2 = precio_bono(y, 6.6), precio_bono(y, 6.85)
carry_trim = (6.0 - r * P1) / 4
print(f"bond price at 6.6 years {P1:.3f}, at 6.85 years {P2:.3f}")
print(f"one more quarter of bond: {P2 - P1:+.3f}")
print(f"one quarter of carry (coupon − financing): "
      f"{-carry_trim:+.3f}")
```
```output
bond price at 6.6 years 112.916, at 6.85 years 113.359
one more quarter of bond: +0.444
one quarter of carry (coupon − financing): -0.408
```

With the yield of the `TPC` bond that day (3.75%, in `tipos3_dv01.csv`), one more quarter of bond is worth about 0.44
points more and one quarter of carry subtracts about 0.41: the two effects almost cancel and the `TPC` curve barely
rises (113.04688, 113.07812, 113.125). The calculation is approximate (annual coupon, same yield for both bonds), but
it explains why in bond futures the slope between expiries is not the carry: in reality, moreover, the cheapest to
deliver bond (CTD) can change from one expiry to another, and the curve with it. Lesson 3.2.4 comes back to bonds with
their DV01.

## Commodities: storage and convenience yield

In a commodity nobody publishes the convenience yield. It is read from the curve. The header figure plots the crude oil
curve on four dates:

```python
fechas = ["2020-04-20", "2022-06-08", "2024-03-13", DIA]
for dia in fechas:
    d = curva("PEC", dia)
    meses = (d["dias"] / 30.44).round(1)
    rel = 100 * np.log(d["liquidacion"]
                       / d["liquidacion"].iloc[0])
    print(dia, f"F1 = {d['liquidacion'].iloc[0]:.2f}",
          " ".join(f"{a:.0f}m:{b:+.1f}" for a, b
                   in zip(meses.iloc[[2, 5, 11, -1]],
                          rel.iloc[[2, 5, 11, -1]])))
```
```output
2020-04-20 F1 = 26.82 2m:+15.3 5m:+31.9 11m:+51.6 17m:+62.6
2022-06-08 F1 = 130.49 2m:-3.9 6m:-8.3 11m:-14.6 17m:-18.7
2024-03-13 F1 = 52.82 2m:+2.3 5m:+5.7 11m:+12.3 17m:+18.5
2025-11-26 F1 = 76.69 3m:-6.0 6m:-11.1 12m:-14.3 17m:-13.2
```

On April 20, 2020, with the first contract at 26.82 USD, the 17-month one trades 62.6% (in logs) above it: it is the
contango of a market with excess supply, that of the pandemic script in the data. In June 2022, with the first one at
130.49, the curve slopes down: the far contracts trade up to 18.7% below. In March 2024, mild contango; in November
2025, backwardation again. The slope between the first two contracts, annualized, gives the convenience yield net of
storage:

```python
def pendiente(mercado, dia, a=0, b=1):
    """ln(F_a / F_b) / (τ_b − τ_a): > 0 in backwardation."""
    d = curva(mercado, dia)
    fa, fb = d["liquidacion"].iloc[[a, b]]
    ta, tb = d["dias"].iloc[[a, b]] / 365
    return np.log(fa / fb) / (tb - ta)

for dia in fechas:
    r = tipos.loc[dia, "usd_1m"] / 100
    k = -pendiente("PEC", dia)          # ln(F2/F1)/Δτ = r + u − y
    print(dia, f"ln(F2/F1)/Δτ {100 * k:+.1f} %,",
          f"rate {100 * r:.2f} %,",
          f"y − u {100 * (r - k):+.1f} %")
```
```output
2020-04-20 ln(F2/F1)/Δτ +96.5 %, rate 0.21 %, y − u -96.3 %
2022-06-08 ln(F2/F1)/Δτ -24.1 %, rate 1.56 %, y − u +25.6 %
2024-03-13 ln(F2/F1)/Δτ +13.5 %, rate 5.30 %, y − u -8.2 %
2025-11-26 ln(F2/F1)/Δτ -38.6 %, rate 3.87 %, y − u +42.4 %
```

The slope is $r + u - y$, so $y - u = r - \text{slope}$. In April 2020, $y - u = -96.3$%: storing a barrel costs far
more than holding it yields. In March 2024, −8.2%: with the rate at 5.30%, the curve rises 13.5% a year, more than the
financing, because storage costs money. In June 2022 (+25.6%) and November 2025 (+42.4%), holding the barrel is worth a
lot: the market pays for product available now and the curve slopes down. The convenience yield is the models' way of
saying “scarcity”.

Measuring the slope of the 23 markets over the whole sample needs the series of the contract held and that of the
next one. Its annual carry is the slope between the two (positive in backwardation, as in lesson 2.3.4):

$$
c_t = \frac{\ln\left(F_{1,t} / F_{2,t}\right)}{\tau_2 - \tau_1}
$$

```python
def serie(mercado):
    c = curvas[curvas["mercado"] == mercado]
    P = c.pivot(index="fecha", columns="contrato",
                values="liquidacion")
    D = c.pivot(index="fecha", columns="contrato", values="dias")
    tiene = que_se_tiene(P.index, mercado)
    i = P.columns.get_indexer(tiene)
    filas = np.arange(len(P))
    Pv = P.to_numpy()
    ayer = np.r_[i[0], i[:-1]]
    antes = np.r_[np.nan, Pv[filas[1:] - 1, ayer[1:]]]
    cm = ctr.loc[ctr["mercado"] == mercado, "contrato"].to_numpy()
    siguiente = dict(zip(cm[:-1], cm[1:]))
    sig = P.columns.get_indexer([siguiente.get(t) for t in tiene])
    ok = sig >= 0
    F2 = np.where(ok, Pv[filas, np.maximum(sig, 0)], np.nan)
    t1 = D.to_numpy()[filas, i] / 365
    t2 = np.where(ok, D.to_numpy()[filas, np.maximum(sig, 0)],
                  np.nan) / 365
    return pd.DataFrame({
        "contrato": tiene, "precio": Pv[filas, i],
        "ret": np.log(Pv[filas, ayer] / antes),
        "carry": np.log(Pv[filas, i] / F2) / (t2 - t1)},
        index=P.index)

mercados = esp[esp["mercado_base"].isna()].index
S = {m: serie(m) for m in mercados}
S["GNA"].loc["2025-11-07":"2025-11-14"].round(4)
```
```output
           contrato  precio     ret   carry
fecha
2025-11-07   GNAZ25   8.700 -0.0221  0.3067
2025-11-10   GNAZ25   8.681 -0.0022  0.2743
2025-11-11   GNAZ25   8.397 -0.0333  0.1896
2025-11-12   GNAZ25   8.089 -0.0374  0.1443
2025-11-13   GNAZ25   8.123  0.0042  0.2365
2025-11-14   GNAZ25   8.024 -0.0123  0.2217
```

`ret` is the return of the contract held and `carry`, the annualized slope towards the next one: in the second week of
November 2025, December gas trades above January gas and the carry is positive, between 14% and 31% a year, and it
jumps from one day to the next because two contracts a month apart amplify any relative move. With the 23 series, the
frequency of contango and the average carry:

```python
carry = pd.DataFrame({m: S[m]["carry"] for m in mercados})
tabla = pd.DataFrame({
    "sector": esp.loc[mercados, "sector"],
    "contango_pct": 100 * (carry < 0).mean(),
    "carry_medio": 100 * carry.mean(),
    "carry_dt": 100 * carry.std(),
})
tabla.loc[["GNA", "GRM", "PEC", "ELB", "MTO", "DQE", "RVA", "TPC",
           "RVE", "RVH"]].round(1)
```
```output
        sector  contango_pct  carry_medio  carry_dt
GNA    energia          61.0        -16.5      92.6
GRM  agricolas          57.3         -4.3      34.5
PEC    energia          46.2         -3.6      29.9
ELB    energia          50.8         -2.7     200.2
MTO    metales          64.8         -1.4       1.8
DQE    divisas          85.2         -1.0       1.0
RVA    indices          33.6          0.3       2.0
TPC      tipos          26.8          0.9       1.4
RVE    indices          12.1          2.1       1.3
RVH    indices           4.2          3.4       1.5
```

Three families:

- **Storable commodities**: contango on around half the days or more (from 46.2% for crude oil to 61.0% for gas) and
  negative average carry: −3.6% in crude oil, −4.3% in corn, −16.5% in gas. Gold, with an average carry of −1.4% and a
  standard deviation of 1.8 points, is almost pure financing.
- **Financials**: the slope is $r - q$ or its equivalent, stable (standard deviation of 1 to 2 points). `RVH`, with a
  high dividend, is in backwardation almost all the time (contango on 4.2% of days); the currency `DQE`, in contango
  85.2%, because the dollar rate is usually above the evro rate in the data.
- **Markets with seasonality in the curve**: electricity (`ELB`) has a carry standard deviation of 200 points and gas
  of 92.6: its slope between two consecutive months jumps with the season. The next section explains why that part is
  not carry.

## The seasonal shape is not carry

```python
d = curva("GNA", DIA)
print(d["liquidacion"].iloc[:14].round(3).to_dict())
```
```output
{'GNAF26': 7.239, 'GNAG26': 6.866, 'GNAH26': 6.259, 'GNAJ26': 4.255, 'GNAK26': 4.253, 'GNAM26': 4.385, 'GNAN26': 4.53, 'GNAQ26': 4.597, 'GNAU26': 4.612, 'GNAV26': 4.727, 'GNAX26': 5.907, 'GNAZ26': 6.284, 'GNAF27': 6.561, 'GNAG27': 6.452}
```

The gas curve is not a slope, it is a wave: January 2026 at 7.239, April and May at 4.25, January 2027 at 6.561 again.
Winter gas is more expensive because heating demand is, and that is known today. Between the March contract (6.259) and
the April one (4.255) there is a 38% “backwardation” in one month, but whoever buys April does not expect it to rise to
the March price: they are two different products (gas delivered in March and gas delivered in April).

In the data model that shape is the term $s(\text{month of } T)$ in the log of the price: it depends on the delivery
month, not on time, so a given contract carries it all its life and it gives no return (it is **priced in**). The trap
is to measure the carry between consecutive months and trade it: in gas, the carry of the series flips sign with the
roll calendar. A common alternative is to compare with the same month of the following year, which has the same
seasonal shape; lesson 3.2.6 deals with the seasonality that is tradable, that of spreads.

## Roll yield: what someone holding the future gains or loses

Whoever holds a future and rolls it gains or loses two things: what the price of the contract held moves, and the
difference paid at each roll when selling one contract and buying the next. Calling $P_t$ the price of the contract
held each day (the unadjusted series of lesson 1.4.6):

$$
\underbrace{\sum_t r_t}_{\text{total return}} = \underbrace{\ln\frac{P_T}{P_0}}_{\text{price change}}
+ \underbrace{\sum_{\text{rolls}} \ln\frac{F_{\text{old}}}{F_{\text{new}}}}_{\text{roll yield}}
$$

```mermaid
flowchart LR
  T[Total return] --> P[Price change]
  T --> RY[Roll yield]
  RY --> CO[Contango: −]
  RY --> BA[Backwardation: +]
```

In contango, each roll sells the near contract, cheaper, and buys the next, more expensive: roll yield is negative.
Whether that is a loss or not depends on what the price does. If the price of the front contract rose exactly as much as
the contango implies (the “expectations hypothesis”), the price change would offset the roll yield and the total return
would be zero. The data makes it possible to check market by market:

```python
filas = {}
for m in mercados:
    s = S[m].loc["2011":]
    anios = len(s) / 252
    total = s["ret"].sum()
    precio = np.log(s["precio"].iloc[-1] / s["precio"].iloc[0])
    filas[m] = {"sector": esp.loc[m, "sector"],
                "total": 100 * total / anios,
                "precio": 100 * precio / anios,
                "roll": 100 * (total - precio) / anios,
                "carry_medio": 100 * s["carry"].mean()}
desc = pd.DataFrame(filas).T.infer_objects()
print(desc.loc[["RVA", "RVH", "TPD", "PEC", "GNA", "MTO",
                "GRA"]].round(2).to_string())
print("roll–carry correlation:",
      round(desc["roll"].corr(desc["carry_medio"]), 3))
```
```output
        sector  total  precio   roll  carry_medio
RVA    indices  12.47   12.34   0.13         0.19
RVH    indices   6.98    3.60   3.39         3.44
TPD      tipos   1.31   -0.03   1.34         1.27
PEC    energia  -2.42   -0.01  -2.41        -2.47
GNA    energia -13.89    3.11 -17.00       -15.32
MTO    metales   0.71    1.99  -1.28        -1.44
GRA  agricolas  -6.82   -0.12  -6.69        -5.08
roll–carry correlation: 0.99
```

Everything in percent a year from 2011 to 2025. The measured roll yield matches the average carry of the series
(correlation of 0.99 across the 23 markets): roll yield is nothing but accumulated carry. The interesting column is the
price:

- In **gas** (`GNA`), the price of the contract held rises 3.11% a year, but roll yield is −17.00% and whoever holds the
  future loses 13.89% a year. The figure plots it: 1 USD in 2011 ends at 0.12, while the price seen on screen ends 59%
  higher.
- In **crude oil** (`PEC`), the price ends where it started (−0.01% a year) and the total return is the roll yield,
  −2.41%.
- In **soybean oil** (`GRA`), roll yield of −6.69% and total return of −6.82%.
- In the **indices**, roll yield is small ($r - q$): +0.13% in `RVA`, +3.39% in `RVH`, where the dividend exceeds the
  rate. The total return is mostly that of the price.
- In the **30-year bond** (`TPD`), roll yield (+1.34%) is almost all of the return: the positive carry of holding a
  high-coupon bond financed at low rates.

*Fig. — Synthetic natural gas, 2011-2025: the price of the held contract against what a holder who rolls it earns.* Two lines on a log scale from 1 in January 2011. The price of the held contract ends at 1.59 at the end of 2025, after a weekly low of 0.46 and a high of 2.79; the value of holding the future and rolling it (highlighted) falls almost without pause to 0.04 in 2023 and ends at 0.12. The gap is the accumulated roll yield: gas spends most of the time in contango.

```python
desc.groupby("sector")[["total", "precio", "roll",
                        "carry_medio"]].mean().round(2)
```
```output
           total  precio  roll  carry_medio
sector
agricolas  -6.39   -1.30 -5.09        -4.02
divisas    -1.23   -0.10 -1.12        -1.07
energia    -3.62    1.42 -5.04        -4.55
indices    10.49    9.65  0.84         0.89
metales    -0.12    1.07 -1.19        -1.39
tipos       0.46   -0.14  0.60         0.59
```

In energy and agriculturals, the price barely offsets the contango (+1.42% and −1.30% a year, against a roll yield of
−5.04% and −5.09%). That is the **known answer** of the generator: the expected return of a commodity future is

$$
E[\Delta \ln F] = -k + \sum_j w_j\, e^{-\kappa_Q \tau}\left[(\kappa_Q - \kappa_P)\,x_j + \text{drift}_j\right]
$$

per year, where $k$ is the contango slope the curve has when the factors $x_j$ are at zero. The first term says that
**the contango is lost**: the price does not rise to catch up with it. The second is the carry premium of agriculturals
and energy, where the curve prices in faster reversion ($\kappa_Q$) than the real one ($\kappa_P$): in backwardation,
the future stays cheap. In gold, with no reversion ($\kappa_P = \kappa_Q = 0$), the negative roll yield (−1.28%) is
almost the interest rate: a gold future is financed gold, and whoever buys the physical metal pays the same.

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

In the data, the roll yield of commodities is a real loss or gain because the generator builds it that way. In real
markets, whether contango is offset by rises in spot or not is an empirical question that the literature debates and
that depends on the market and the period. What is an accounting fact in any market is the decomposition: the return
of a future is not the price change on the chart, and comparing a futures system with the price of the commodity is
comparing it with something nobody can buy.

</aside>

## The Samuelson effect: far expiries move less

The curve has one last useful property: not all expiries move the same.

```python
x = curvas[curvas["mercado"].isin(["PEC", "GNA", "GRM", "MTO",
                                   "RVA"])].sort_values(
    ["contrato", "fecha"])
x = x.assign(r=np.log(x["liquidacion"]).groupby(
    x["contrato"]).diff())
x = x[(x["fecha"] >= "2011-01-01") & (x["dias"] > 15)]
x["tramo"] = (x["dias"] // 91) * 3          # months: 0, 3, 6…
vol = (x.groupby(["tramo", "mercado"])["r"].std().unstack()
       * np.sqrt(252))
vol.loc[:18].round(3)
```
```output
mercado    GNA    GRM    MTO    PEC    RVA
tramo
0        0.341  0.254  0.154  0.320  0.188
3        0.229  0.188  0.154  0.248  0.190
6        0.185  0.144  0.154  0.198  0.190
9        0.163  0.123  0.155  0.167  0.190
12       0.146  0.106  0.154  0.146  0.182
15       0.132  0.094  0.155  0.132    NaN
18       0.119  0.091  0.154    NaN    NaN
```

In gas, the annual volatility of the contract goes from 34.1% in the 0-to-3-month bucket to 11.9% at 18; in corn, from
25.4% to 9.1%; in crude oil, from 32.0% to 13.2% at 15 months. In gold and in the index, nothing: around 15.4% and 19%
in every bucket. It is the **Samuelson effect**: in markets where supply and demand shocks correct over time, they move
the price of near delivery a lot and that of delivery a year out little. In the data model, an expiry τ years out
receives $e^{-\kappa_Q \tau}$ of each shock, with κ_Q of up to 4 in gas. The figure shows it by month:

*Fig. — Samuelson effect in the synthetic futures: volatility of each contract by months left, 2011-2025.* Four lines with the annual volatility of daily returns by months to expiry. Gas (highlighted) falls from 41.7% in the first month to 10.3% at 23 months; crude from 35.5% to 12.7% at 17 months; corn from 26.3% to 9.1% at 18 months; gold stays flat around 15.5%.

For a system it has two consequences. The volatility of the continuous series jumps at each roll in markets with
reversion (the new contract is farther out and moves less), so it is better estimated with returns of the same contract
and with a window that is not too long. And choosing a farther expiry reduces the risk per contract, but changes the
bet: in gas, the one-year contract barely responds to what moves the first one. Lesson 3.1.3 measures what happens when
rolling earlier or later.

## Exercises

1. Calculate the implied financing rate (implied *repo*) of the `RVE` futures on November 26, 2025 assuming the
   dividend is known, and compare it with the one-month evro rate.

**Solution.**

```python
S_, q_, r_ = c.loc["RVE", ["contado", "rentabilidad_dividendo",
                          "tipo_1m"]]
d = curva("RVE", DIA)
tau = d["dias"] / 365
d["r_impl"] = 100 * np.log(d["liquidacion"] / S_) / tau + q_
print(f"1-month rate {r_:.2f} %")
d.round(3)
```
```output
1-month rate 2.09 %
          dias  liquidacion  r_impl
contrato
RVEZ25      23       6946.0   1.921
RVEH26     114       6929.0   1.956
RVEM26     204       6914.0   2.005
RVEU26     296       6900.0   2.048
RVEZ26     387       6885.0   2.054
```

It is the same equation solved for the rate. The contracts with more than three months give an implied rate a few
tenths below 2.09%, the 3-to-5 basis point difference between the settlement and the fair value divided by τ; the
23-day one gives a larger error, because one price point (the `RVE` tick) is almost 1.5 basis points and it is divided
by a τ of 0.063. In real markets, that implied rate is compared with each participant's cost of financing: if it is
higher, it pays to sell the future and buy the index (*cash and carry*).

2. Repeat the decomposition into total return, price and roll yield for crude oil (`PEC`) for 2020 only and for 2022
   only. When did roll yield help and when did it hurt?

**Solution.**

```python
for a in ["2020", "2022"]:
    s = S["PEC"].loc[a]
    total = s["ret"].sum()
    precio = np.log(s["precio"].iloc[-1] / s["precio"].iloc[0])
    roll = total - precio
    print(a, f"total {100 * total:+.1f} %, price "
          f"{100 * precio:+.1f} %, roll {100 * roll:+.1f} %")
```
```output
2020 total -163.5 %, price -89.1 %, roll -74.3 %
2022 total -22.9 %, price -26.1 %, roll +3.2 %
```

In 2020, with the curve in steep contango during the fall, the roll subtracts 74.3 log points: the price of the
contract held falls 89.1% in logs and whoever held the future, 163.5% (of each dollar about 20 cents were left). In
2022, in backwardation, the roll adds 3.2 points: the future lost less (−22.9%) than the price (−26.1%). It is the same
mechanism as in gas, with each year's sign.

3. Why can the volatility of a continuous gas series built with the contract held jump at each roll, and how does it
   affect a system that sizes by volatility (lesson 2.2.2)?

**Solution.**

Because of the Samuelson effect: the contract held gets closer and closer to expiry and its volatility rises; at the
roll you move to one a month farther out, which moves less. A volatility estimated with long windows mixes stretches of
both, and one that uses returns of the new contract before holding it underestimates them. A system that sizes by
volatility buys more contracts just after the roll (low vol) and fewer at the end of the contract's life (high vol).
It is not an error in itself, because the real risk changes that way, but it has to be estimated with returns of the
same contract and with a window consistent with the life of a contract.

## Self-assessment

1. On November 26, 2025 the RVA index is at 7,841.33, the 1-month rate at 3.87% and the dividend yield at 1.891%. Where should the 114-day future trade?

   - A. Below spot, because the dividend lowers the price
   - B. At the same price as spot
   - C. Where the market expects the index to be in March
   - D. About 48 points above: S·e^((r − q)τ) with r − q = 1.98% and τ = 114/365 gives 7,889.95; the contract settles at 7,892

   *Answer D: About 48 points above: S·e^((r − q)τ) with r − q = 1.98% and τ = 114/365 gives 7,889.95; the contract settles at 7,892.* In a financial future the curve is set by arbitrage: whoever buys the index pays the rate and collects the dividend; whoever buys the future does neither. The fair value is 7,889.95 and the contract settles 2.60 basis points above it.

2. The crude oil curve on April 20, 2020 has a slope of +96.5% a year between the first two contracts with a rate of 0.21%. What does it say about the net convenience yield (convenience minus storage)?

   - A. That it is very positive: holding physical crude is valuable
   - B. That it is −96.3%: storage costs far more than holding the barrel yields, the situation of a market with excess supply and full storage tanks
   - C. That it is zero
   - D. Nothing: the slope depends only on the interest rate

   *Answer B: That it is −96.3%: storage costs far more than holding the barrel yields, the situation of a market with excess supply and full storage tanks.* With F = S·e^((r + u − y)τ), the slope is r + u − y and the net convenience, y − u = r − slope = 0.21 − 96.5. A contango like that only exists when there is too much product and storing it is expensive.

3. In the synthetic natural gas, from 2011 to 2025, the price of the contract held rises 3.11% a year and whoever holds the future loses 13.89% a year. Where does the difference come from?

   - A. From commissions
   - B. From an error in the continuous series
   - C. From roll yield: −17.00% a year, because the curve is almost always in contango and each roll sells the cheap contract that converges and buys the next, more expensive one
   - D. From the initial margin

   *Answer C: From roll yield: −17.00% a year, because the curve is almost always in contango and each roll sells the cheap contract that converges and buys the next, more expensive one.* Total return = price change + roll yield. The price of the front contract does not rise enough to catch up with the contango, so the contango is lost. The gas figure shows it: 1 USD in 2011 ends at 0.12.

4. Why is roll yield negative in gold (−1.28% a year in the data) and yet not a premium collected by whoever sells the future?

   - A. Because in gold the contango is the cost of financing (and storage): whoever holds physical gold pays it anyway, so the future and financed physical gold return the same
   - B. Because gold has no futures
   - C. Because gold is always in backwardation
   - D. Because gold's volatility is zero

   *Answer A: Because in gold the contango is the cost of financing (and storage): whoever holds physical gold pays it anyway, so the future and financed physical gold return the same.* In investment assets that are easy to store, the slope is almost only the rate (and storage). A gold future is equivalent to financed gold: the negative roll yield is the rate that the buyer of the physical metal would also pay.

5. The volatility of the synthetic gas is 41.7% in the contract with less than a month left and 10.3% at 23 months. What is the effect called and what does it imply?

   - A. Calendar effect: gas moves less in summer
   - B. A data error
   - C. Volatility smile
   - D. Samuelson effect: shocks that mean-revert move the near expiry a lot and the far ones little; choosing a farther contract reduces the risk per contract and changes what is being bet on

   *Answer D: Samuelson effect: shocks that mean-revert move the near expiry a lot and the far ones little; choosing a farther contract reduces the risk per contract and changes what is being bet on.* In the data, the curve prices in that shocks revert at speed κ_Q, so an expiry τ years out moves e^(−κ_Q·τ) times as much as spot. In gold, with no reversion, volatility is flat.

## Key points

- The curve of a financial future is set by arbitrage: index S·e^((r − q)τ), currency X·e^((r_USD − r_currency)τ), bond with coupon minus financing. The index futures in the data settle within 5 basis points of their fair value.
- In commodities, F = S·e^((r + u − y)τ): the slope measures storage minus convenience yield. Crude oil goes from +96.5% annual contango in April 2020 to −38.6% in November 2025.
- The seasonal shape of the curve (expensive winter gas, summer electricity) is not carry: it is priced in and gives no return to whoever buys the contract.
- Return of holding a future = price change + roll yield. Across the 23 markets, measured roll yield and average carry have a correlation of 0.99; in gas, roll yield is −17.00% a year.
- In the data, commodity prices do not rise to catch up with the contango: negative roll yield is a real loss (the known answer, −k). Whether contango is a premium in a real market is an empirical question, not a law.
- Samuelson effect: in markets with mean reversion, far expiries move much less than the first one (gas: 41.7% against 10.3%); in gold, not at all.

## Resources

### Research

- [Central Banks and Monetary Policy in Python](https://pythonparatrading.com/en/monetary-policy-in-python/) · Research-0010 · 14 min

### Documentation

- [Eurex: Euro-Bund Futures (6% coupon, deliverables from 8.5 to 10.5 years)](https://www.eurex.com/ex-en/markets/int/long-term-interest-rates/fix/government-bonds/Euro-Bund-Futures-137298) · eurex.com
- [MEFF: IBEX 35 futures (settlement price at expiry, in Spanish)](https://www.meff.es/esp/Derivados-Financieros/Futuros-sobre-IBEX35) · meff.es

### Books

- [Robert Carver: Advanced Futures Trading Strategies (Harriman House, 2023)](https://www.harriman-house.com/authors/robert-carver/advanced-futures-trading-strategies/9780857199683) · harriman-house.com

### Data

- [Level 3 data: futures curves (Parquet)](https://pythonparatrading.com/curso/datos/nivel-3/futuros3_curvas.parquet) · Download · futuros3_curvas.parquet
- [Level 3 data: index spot, dividend and rate (CSV)](https://pythonparatrading.com/curso/datos/nivel-3/futuros3_indices_contado.csv) · Download · futuros3_indices_contado.csv

Previous lesson: [3.1.1 · Main futures contracts](https://pythonparatrading.com/en/python-trading-course/level-3/main-futures-contracts/)

Next lesson: [3.1.3 · Roll management, margins, SPAN and implied leverage](https://pythonparatrading.com/en/python-trading-course/level-3/futures-roll-margins-span-leverage/)
