Lesson 3.1.2 · 65 min

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.

Fig. — The synthetic crude curve on four dates: extreme contango in April 2020, backwardation in 2022 and 2025, moderate contango in 2024
Level 3 lessons
  1. 3.1 · Futures in depth

    1. 3.1.1 Main futures contracts
    2. 3.1.2 Futures curve: basis, cost of carry, contango and roll yield
    3. 3.1.3 Roll management, margins, SPAN and implied leverage
    4. 3.1.4 Diversified futures systems: Carver's approach
  2. 3.2 · Futures spreads

    1. 3.2.1 Calendar spreads, butterflies and condors
    2. 3.2.2 Intercommodity spreads: crack, spark and crush
    3. 3.2.3 Rate and index spreads: 2s10s, NOB, NQ/ES and Europe versus the U.S.
    4. 3.2.4 Hedge ratios: by notional, by DV01 and by volatility
    5. 3.2.5 Spread data and backtesting: listed, synthetic and legged
    6. 3.2.6 Seasonality of spreads: multi-year analysis, patterns and traps
  3. 3.3 · Advanced forex

    1. 3.3.1 FX microstructure: OTC, ECN, last look and sessions
    2. 3.3.2 Carry and crash risk, momentum and value in currencies; baskets
    3. 3.3.3 Currency risk in multi-currency portfolios; execution via OANDA, IBKR or MT5
  4. 3.4 · Option theory and pricing

    1. 3.4.1 Payoffs, put-call parity and arbitrage bounds
    2. 3.4.2 Binomial trees, Black-Scholes-Merton, Black-76 and Monte Carlo
    3. 3.4.3 Greeks: delta, gamma, vega, theta, rho, vanna, volga and charm
    4. 3.4.4 Implied volatility: Newton-Raphson, Brent, py_vollib and QuantLib
    5. 3.4.5 Smile, skew and term structure; volatility surface with SVI
    6. 3.4.6 Realized vs implied volatility; volatility risk premium; VIX and VSTOXX
    7. 3.4.7 Advanced models: Heston, SABR and local volatility
  5. 3.5 · Option structures in Python

    1. 3.5.1 Directional and vertical structures: call, put, covered call, collar and spreads
    2. 3.5.2 Volatility and range structures: straddles, strangles, condors and butterflies
    3. 3.5.3 Calendars, diagonals, ratios, backspreads, risk reversals and jade lizard
    4. 3.5.4 A custom tool: payoff, aggregate Greeks and scenarios
    5. 3.5.5 Management: adjustments, rolls, early closes, early assignment and pin risk
  6. 3.6 · Systematic volatility and options strategies

    1. 3.6.1 Systematic premium selling: put-write, strangles and their tail risk
    2. 3.6.2 Delta hedging, gamma scalping and dispersion
    3. 3.6.3 VIX and VSTOXX strategies: volatility futures and ETPs
    4. 3.6.4 Options backtesting: data, fills and event adjustments
  7. 3.7 · Crypto derivatives and specific strategies

    1. 3.7.1 Perpetuals: funding mechanics, mark price and liquidations
    2. 3.7.2 Funding arbitrage, basis with futures and cross-exchange arbitrage
    3. 3.7.3 Crypto options, on-chain data and DeFi
    4. 3.7.4 ccxt for trading on multiple exchanges
    5. 3.7.5 MiCA: the EU framework for crypto-assets and their service providers (CASPs)

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)=St e(r+u−y) τ,τ=T−tF(t, T) = S_t \, e^{(r + u - y)\,\tau}, \qquad \tau = T - t

with rr the financing rate, uu the storage cost and yy 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).

Spot+ financing r+ storage u− what it yields yFuture τ years outIndex: dividendCurrency: its rateConvenience yield
Mermaid source
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>yr + 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:

IN [01]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"]]
OUT [01]Contracts, curves and the index spot
         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:

IN [02]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")
OUT [02]Fair value of the RVA and RVE futures
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.98r - 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.86r - 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:

qimpl=r−ln⁡(F/S)τq_{\text{impl}} = r - \frac{\ln(F/S)}{\tau}
IN [03]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)
OUT [03]Dividend implied by the future
     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:

IN [04]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())
OUT [04]Curve of the 10-year note and of the evro
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:

IN [05]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}")
OUT [05]Two effects on the bond curve
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:

IN [06]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]])))
OUT [06]The crude oil curve on four dates
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:

IN [07]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} %")
OUT [07]Annualized slope and net convenience yield
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−yr + u - y, so y−u=r−slopey - u = r - \text{slope}. In April 2020, y−u=−96.3y - 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):

ct=ln⁡(F1,t/F2,t)τ2−τ1c_t = \frac{\ln\left(F_{1,t} / F_{2,t}\right)}{\tau_2 - \tau_1}
IN [08]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)
OUT [08]The contract held and its carry
           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:

IN [09]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)
OUT [09]How many days each market is in contango
        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−qr - 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

IN [10]python
d = curva("GNA", DIA)
print(d["liquidacion"].iloc[:14].round(3).to_dict())
OUT [10]The natural gas curve
{'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(month of T)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 PtP_t the price of the contract held each day (the unadjusted series of lesson 1.4.6):

∑trt⏟total return=ln⁡PTP0⏟price change+∑rollsln⁡FoldFnew⏟roll yield\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}}
Total returnPrice changeRoll yieldContango: −Backwardation: +
Mermaid source
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:

IN [11]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))
OUT [11]Total return = price change + roll yield
        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−qr - 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
IN [12]python
desc.groupby("sector")[["total", "precio", "roll",
                        "carry_medio"]].mean().round(2)
OUT [12]By sector
           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[Δln⁡F]=−k+∑jwj e−κQτ[(κQ−κP) xj+driftj]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 kk is the contango slope the curve has when the factors xjx_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 (κQ\kappa_Q) than the real one (κP\kappa_P): in backwardation, the future stays cheap. In gold, with no reversion (κP=κQ=0\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.

The Samuelson effect: far expiries move less

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

IN [13]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)
OUT [13]Volatility by months to expiry
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−κQτ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

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
IN [14]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)
OUT [14]Rate implied by the RVE curve
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).

  1. 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
IN [15]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} %")
OUT [15]Crude oil: 2020 against 2022
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.

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

Each answer is folded under its question

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?

    • ABelow spot, because the dividend lowers the price
    • BAt the same price as spot
    • CWhere the market expects the index to be in March
    • DAbout 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
    Show answer

    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)?

    • AThat it is very positive: holding physical crude is valuable
    • BThat 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
    • CThat it is zero
    • DNothing: the slope depends only on the interest rate
    Show answer

    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?

    • AFrom commissions
    • BFrom an error in the continuous series
    • CFrom 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
    • DFrom the initial margin
    Show answer

    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?

    • ABecause 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
    • BBecause gold has no futures
    • CBecause gold is always in backwardation
    • DBecause gold's volatility is zero
    Show answer

    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?

    • ACalendar effect: gas moves less in summer
    • BA data error
    • CVolatility smile
    • DSamuelson 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
    Show answer

    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

Data