---
title: "3.2.4 Ratios de cobertura: nominal, DV01 y volatilidad"
description: "Cuántos contratos de cada pata lleva un spread: ratios por nominal, DV01, volatilidad y mínima varianza, qué riesgo deja cada uno y cómo redondear."
url: "https://pythonparatrading.com/curso-python-trading/nivel-3/ratios-de-cobertura-nominal-dv01-y-volatilidad/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-3/ratios-de-cobertura-nominal-dv01-y-volatilidad.png"
---

# 3.2.4 · Ratios de cobertura: por nominal, por DV01 y por volatilidad

> Un spread no es solo «largo de uno y corto de otro»: el número de contratos de cada pata decide qué riesgo queda. Esta lección calcula los ratios por valor nominal, por DV01, por volatilidad y por mínima varianza en los futuros de tipos, de índices y de energía del nivel, y mide con los datos lo que cada cobertura deja dentro.

- Curso: [Nivel 3: Derivados: futuros, spreads, forex y opciones](https://pythonparatrading.com/curso-python-trading/nivel-3/)
- Módulo: 3.2 · Spreads de futuros
- Duración: 60 min
- Requisitos: [1.2.3 · Futuros: especificaciones, margen, roll y contango](https://pythonparatrading.com/curso-python-trading/nivel-1/futuros-especificaciones-margen-y-roll/), [1.2.7 · Renta fija y tipos de interés; CFDs y productos apalancados](https://pythonparatrading.com/curso-python-trading/nivel-1/renta-fija-tipos-y-productos-apalancados/), [2.1.6 · Filtro de Kalman para hedge ratios dinámicos](https://pythonparatrading.com/curso-python-trading/nivel-2/filtro-de-kalman-hedge-ratio-dinamico/), [3.2.1 · Spreads de calendario, butterflies y condors](https://pythonparatrading.com/curso-python-trading/nivel-3/spreads-de-calendario-butterflies-y-condors/)

## Al terminar esta lección

- Calcular el ratio de cobertura de un spread por valor nominal, por DV01, por volatilidad y por mínima varianza, con los multiplicadores de cada contrato.
- Medir con una regresión sobre nivel y pendiente qué exposición deja cada ratio en un spread de tipos, y contrastarla con la respuesta conocida.
- Explicar por qué el ratio por volatilidad falla cuando una pata deja de moverse, y por qué el de DV01 es estable.
- Redondear un ratio a contratos enteros, elegir el tamaño mínimo que lo respeta y decidir cada cuánto reequilibrarlo.
- Elegir el ratio según lo que se quiere operar: tipos, dirección del mercado, dinero en riesgo o unidades físicas.

## Qué iguala cada ratio

Un spread de futuros tiene dos patas: se compra una y se vende la otra. La pregunta práctica es **cuántos contratos
de cada una**. Esa cifra, el **ratio de cobertura** $h$ (contratos de una pata por cada contrato de la otra), decide
qué riesgo se cancela y cuál queda. Con $F$ el precio, $m$ el multiplicador del contrato (lo que vale un punto de
precio), $\mathrm{DV01}$ la variación en dinero de un contrato por un punto básico de tipos y $\sigma$ la volatilidad
del rendimiento, los cuatro ratios de esta lección, para cubrir un contrato de la pata $L$ con contratos de la
pata $C$, son:

$$
h_{\text{nominal}} = \frac{F_L\,m_L}{F_C\,m_C}, \qquad
h_{\mathrm{DV01}} = \frac{\mathrm{DV01}_L}{\mathrm{DV01}_C}, \qquad
h_{\sigma} = \frac{\sigma_L\,F_L\,m_L}{\sigma_C\,F_C\,m_C}, \qquad
h_{\mathrm{MV}} = \frac{\operatorname{Cov}(\Delta V_L, \Delta V_C)}{\operatorname{Var}(\Delta V_C)}
$$

con $\Delta V$ el cambio diario del valor de un contrato en dinero. El primero iguala el dinero invertido; el segundo,
la sensibilidad a un desplazamiento de tipos de un punto básico; el tercero, el dinero en riesgo con la volatilidad
reciente; el cuarto, el de **mínima varianza**, es la pendiente de una regresión: el que habría dejado el menor riesgo
total en la ventana de estimación (la lección 2.1.6 lo hace dinámico con un filtro de Kalman).

| Ratio | Qué iguala | Qué necesita | Cuándo tiene sentido |
|---|---|---|---|
| Nominal | Dinero de cada pata | Precio y multiplicador | Rendimiento relativo de dos activos parecidos |
| DV01 | Sensibilidad a 1 pb | DV01 por contrato | Spreads de tipos: aislar la pendiente |
| Volatilidad | Dinero en riesgo reciente | σ estimada | Patas sin relación estructural |
| Mínima varianza | Riesgo total pasado | Regresión de P&L | Reducir riesgo, sin decir cuál |
| Unidades | Cantidad física | Ficha del contrato | Márgenes físicos: crack, crush, spark |

Ninguno es «el correcto» en abstracto. La elección depende de qué se quiere dejar fuera del spread:

```mermaid
flowchart TB
  Q{¿Qué cancelas?} -- tipos --> D[DV01]
  Q -- mercado --> B[Beta]
  Q -- margen físico --> U[Unidades]
  Q -- riesgo total --> V[Volatilidad o MV]
```

Los datos de esta lección son **sintéticos**: futuros de tipos, índices y energía ficticios que imitan a los reales
(`TPA` al de 2 años, `TPC` al de 10, `TPD` al bono a 30 años, `RVA` y `RVB` a los mini del S&P 500 y del Nasdaq-100,
`PEC` y `PEG` al crudo y a la gasolina). Su ventaja es que la respuesta se conoce: el archivo `tipos3_dv01.csv` trae el
DV01 exacto de cada contrato con el modelo que generó los precios, y la beta del Nasdaq ficticio sobre el factor de
mercado está fijada en 1,15.

## Tres patas de tipos y su DV01

En los futuros de bonos, el DV01 de un contrato es la variación de su precio por un punto básico de rendimiento del
bono que lo determina (en los contratos reales, el más barato de entregar; aquí, un bono estándar por contrato), en
dinero. La lección 1.2.7 lo calculó para un bono; `tipos3_dv01.csv` lo da para el contrato que se tiene cada sesión:

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

dv = pd.read_csv("tipos3_dv01.csv", parse_dates=["fecha"])
esp = pd.read_csv("futuros3_especificaciones.csv",
                  index_col="mercado")
mult = esp["multiplicador"]

fin = dv[dv["fecha"] == "2025-12-31"].set_index("mercado")
fin["nominal"] = fin["liquidacion"] * mult.reindex(fin.index)
fin[["contrato", "liquidacion", "rendimiento_ctd",
     "duracion_modificada", "dv01", "nominal"]].round(2)
```
```output
        contrato  liquidacion  rendimiento_ctd  duracion_modificada    dv01    nominal
mercado
TPA       TPAH26       104.27             3.67                 2.04   42.55  208546.88
TPB       TPBH26       108.75             3.73                 3.97   43.21  108750.00
TPC       TPCH26       112.55             3.83                 5.73   64.47  112546.88
TPD       TPDH26       121.62             4.13                11.06  134.46  121625.00
TPE       TPEH26       126.91             2.51                 7.24   91.88  126910.00
```

El contrato a 2 años (`TPA`) tiene el doble de nominal que los demás (el multiplicador es 2.000 USD por punto de
precio, frente a 1.000), pero su duración es de 2 años: su DV01, 42,55 USD por pb, es menos de un tercio del del bono
a 30 años (134,46). Igualar nominales e igualar sensibilidades a los tipos dan, por tanto, cifras muy distintas.

Para medir el P&L de cada pata hace falta el cambio de precio del **mismo contrato** de un día a otro: el de la serie
del contrato que se tiene salta en cada roll, cuando se pasa de un vencimiento al siguiente (lección 1.4.6). La función
siguiente toma el contrato que se tenía ayer y busca su precio de ayer y de hoy en `futuros3_curvas.parquet`:

```python
cur = pd.read_parquet("futuros3_curvas.parquet")
px = cur.set_index(["fecha", "contrato"])["liquidacion"]

def cambio_mismo_contrato(contratos):
    """Cambio de hoy del contrato que se tenía ayer: sin el salto
    del roll (lección 1.4.6)."""
    ayer = contratos.shift(1).iloc[1:]
    f, f0 = contratos.index[1:], contratos.index[:-1]
    hoy = px.reindex(list(zip(f, ayer))).to_numpy()
    antes = px.reindex(list(zip(f0, ayer))).to_numpy()
    return pd.Series(hoy - antes, index=f)

D = dv.pivot(index="fecha", columns="mercado", values="dv01")
F = dv.pivot(index="fecha", columns="mercado", values="liquidacion")
Y = dv.pivot(index="fecha", columns="mercado",
             values="rendimiento_ctd")
C = dv.pivot(index="fecha", columns="mercado", values="contrato")
dP = pd.DataFrame({k: cambio_mismo_contrato(C[k])
                   for k in ["TPA", "TPB", "TPC", "TPD"]})
usd = dP * mult[dP.columns]         # USD por contrato y sesión
ret = dP / F[dP.columns].shift(1)   # rendimiento del contrato
usd.loc["2025-12-24":].round(1)
```
```output
              TPA    TPB     TPC     TPD
fecha
2025-12-24   70.3   70.3    15.6  -312.5
2025-12-26  265.6   85.9  -109.4  -843.8
2025-12-29  328.1  140.6     0.0  -468.8
2025-12-30  507.8  656.2  1062.5  2250.0
2025-12-31 -500.0 -437.5  -578.1 -1000.0
```

Las cifras son múltiplos del valor del tick de cada contrato (15,625 USD en `TPC`, 31,25 en `TPD`, 7,8125 en `TPA`):
las liquidaciones se redondean al tick, como en un mercado real.

## Cuatro ratios para el 2s10s y el NOB

Dos spreads de tipos clásicos sirven de laboratorio. El **2s10s** opone la nota a 2 años a la de 10; el **NOB**
(*notes over bonds*), la de 10 años al bono a 30. La lección 3.2.3 los construye y estudia su comportamiento; aquí se
trata solo de cuántos contratos lleva cada pata. En los dos, el ratio se expresa como contratos del plazo corto por
cada contrato del largo:

```python
def ratios(corto, largo):
    """Contratos de la pata corta por contrato de la larga."""
    nominal = (F[largo] * mult[largo]) / (F[corto] * mult[corto])
    dv01 = D[largo] / D[corto]
    sig = ret.rolling(63).std()
    vol = nominal * sig[largo] / sig[corto]
    mv = (usd[largo].rolling(252).cov(usd[corto])
          / usd[corto].rolling(252).var())
    return pd.DataFrame({"nominal": nominal, "dv01": dv01,
                         "volatilidad": vol, "min_varianza": mv})

R = {"2s10s": ratios("TPA", "TPC"), "nob": ratios("TPC", "TPD")}
pd.DataFrame({k: v.iloc[-1] for k, v in R.items()}).round(2)
```
```output
              2s10s   nob
nominal        0.54  1.08
dv01           1.52  2.09
volatilidad    1.41  2.15
min_varianza   1.18  2.03
```

En el 2s10s, el nominal pide medio contrato a 2 años por cada contrato a 10 (el de 2 años tiene el doble de nominal)
y el DV01, uno y medio: casi el triple. En el NOB, el nominal pide algo más de uno y el DV01 algo más de dos. La
volatilidad de 63 sesiones y la regresión de 252 sesiones dan, a fin de 2025, valores cercanos al de DV01, porque en
2025 los dos plazos se mueven de forma parecida en puntos básicos. No siempre es así, como se ve más abajo.

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

Las bolsas listan spreads de tipos con proporciones fijas de contratos. La documentación de CME Globex cita el NOB y
otros spreads intercommodity de CBOT entre los que tienen una pata «en proporción mayor que uno» (enlace en los
recursos). Una proporción fija del mercado es cómoda (se negocia como un instrumento), pero se fija de antemano: entre
revisiones, el DV01 de las patas sigue moviéndose.

</aside>

## Lo que deja cada cobertura

Un empinador (*steepener*) gana si la curva se empina: compra $h$ contratos del plazo corto y vende uno del largo. Su
P&L diario por contrato largo es $h\,\Delta V_C - \Delta V_L$, con $h$ el ratio **de la sesión anterior** (el de hoy
usaría el cierre de hoy: anticipación). Para saber qué riesgo queda, se regresa ese P&L sobre el cambio del rendimiento
de la pata larga (el nivel) y el de la pendiente (largo menos corto), en puntos básicos:

$$
\mathrm{P\&L}_t = a + \beta_{\text{nivel}}\,\Delta y^{L}_t + \beta_{\text{pend}}\,\Delta\left(y^{L}_t - y^{C}_t\right) + \varepsilon_t
$$

**La respuesta conocida**: con la cobertura por DV01, un desplazamiento paralelo no gana ni pierde, así que
$\beta_{\text{nivel}} \approx 0$, y $\beta_{\text{pend}}$ es el DV01 de la pata larga (un punto básico más de
pendiente, con el plazo corto quieto, es un punto básico más en la pata vendida). Con la beta al nivel sola, sin la
pendiente en la regresión, no tiene por qué salir 0: en el modelo de los datos, nivel y pendiente están correlados.

```python
PATAS = {"2s10s": ("TPA", "TPC"), "nob": ("TPC", "TPD")}

def betas(nombre, ratio, desde, hasta):
    corto, largo = PATAS[nombre]
    h = R[nombre][ratio].shift(1)       # el de la sesión anterior
    pnl = h * usd[corto] - usd[largo]   # empinador
    nivel = Y[largo].diff() * 100       # pb
    pend = (Y[largo] - Y[corto]).diff() * 100
    m = pd.concat([pnl, nivel, pend], axis=1).loc[desde:hasta]
    m = m.dropna()
    m.columns = ["pnl", "nivel", "pend"]
    X = np.c_[np.ones(len(m)), m["nivel"], m["pend"]]
    b = np.linalg.lstsq(X, m["pnl"], rcond=None)[0]
    sola = np.polyfit(m["nivel"], m["pnl"], 1)[0]
    return {"beta_sola": sola, "beta_nivel": b[1],
            "beta_pend": b[2], "desv_dia": m["pnl"].std()}

filas = {(n, r, e): betas(n, r, *p)
         for n in PATAS
         for e, p in {"2010-15": ("2010", "2015"),
                      "2016-25": ("2016", "2025")}.items()
         for r in R[n].columns}
tabla = pd.DataFrame(filas).T
tabla.index.names = ["spread", "ratio", "periodo"]
tabla.round(1)
```
```output
                             beta_sola  beta_nivel  beta_pend  desv_dia
spread ratio        periodo
2s10s  nominal      2010-15       57.9        45.5       26.0     365.6
       dv01         2010-15       34.0        -0.2       71.7     269.4
       volatilidad  2010-15       11.9       -33.4       93.1     257.9
       min_varianza 2010-15       19.8       -22.7       88.0     254.9
       nominal      2016-25       50.1        44.8       26.3     373.6
       dv01         2016-25       15.1         1.1       69.5     300.7
       volatilidad  2016-25       11.8        -0.5       61.1     300.3
       min_varianza 2016-25       20.8         9.0       58.4     296.5
nob    nominal      2010-15       82.8        76.2       80.2     557.7
       dv01         2010-15       13.0         0.1      156.6     361.0
       volatilidad  2010-15       11.1        -2.0      160.2     370.7
       min_varianza 2010-15       23.0        10.9      148.9     372.2
       nominal      2016-25       86.7        79.7       83.1     676.7
       dv01         2016-25       14.2         1.0      158.2     412.7
       volatilidad  2016-25        9.3        -4.1      160.2     420.2
       min_varianza 2016-25       18.1         5.2      154.5     421.0
```

*Fig. — Beta al nivel de tipos del P&L diario del empinador con cada ratio, con la pendiente en la regresión: círculo hueco 2010-2015, lleno 2016-2025.* Dos paneles de puntos, 2s10s y NOB, con cuatro filas. La cobertura por DV01, destacada, queda pegada a cero en los dos periodos: −0,2 / 1,1 USD por pb en el 2s10s y 0,1 / 1,0 en el NOB. La de nominal queda lejos: 45,5 / 44,8 y 76,2 / 79,7. La de volatilidad da −33,4 / −0,5 en el 2s10s y la de mínima varianza −22,7 / 9,0: cambian de signo entre periodos.

La tabla tiene cuatro lecturas:

- **DV01 recupera la respuesta conocida.** La beta al nivel es −0,2 y 1,1 USD por pb en el 2s10s y 0,1 y 1,0 en el
  NOB, en los dos periodos; la beta a la pendiente, 71,7 y 69,5 en el 2s10s y 156,6 y 158,2 en el NOB, es el DV01
  medio de la pata larga (unos 70 en `TPC` y unos 160 en `TPD`, según `tipos3_dv01.csv`). Es lo que se busca: un
  spread que solo responde a la pendiente.
- **El nominal deja una apuesta de duración.** Con 0,54 contratos a 2 años, el empinador tiene una beta al nivel de
  45 USD por pb (80 en el NOB) y solo 26 a la pendiente: responde más a un desplazamiento paralelo que a lo que se
  quería operar.
- **Volatilidad y mínima varianza cambian de signo.** En 2010-2015 la cobertura por volatilidad tiene una beta al
  nivel de −33,4 y en 2016-2025, de −0,5; la de mínima varianza pasa de −22,7 a +9,0. Reducen el riesgo diario (su
  desviación es la más baja: 257,9 y 254,9 USD en 2010-2015 frente a 269,4 con DV01), pero lo hacen mezclando una
  apuesta de nivel que nadie pidió, y de signo variable.
- **La beta «sola» engaña.** Sin la pendiente en la regresión, incluso el empinador por DV01 parece tener una beta al
  nivel de 34 USD por pb en 2010-2015. No es un fallo de la cobertura: en esos años, cerca de tipos cero, el plazo de
  2 años apenas se mueve, y cuando el nivel sube la curva se empina a la vez. Una regresión que omite una variable
  correlada atribuye su efecto a la que queda.

## El ratio se mueve

La figura de la cabecera sigue los ratios del 2s10s mes a mes de 2010 a 2025. El de nominal apenas cambia (entre
0,52 y 0,63) y el de DV01 tampoco mucho (entre 1,42 y 1,83): la duración de cada contrato varía despacio, con el
nivel de los tipos y con el bono que lo determina en cada vencimiento. El de volatilidad, en cambio, salta:

```python
anual = R["2s10s"].resample("YE").last()
anual.index = anual.index.year
anual.loc[[2010, 2011, 2012, 2015, 2019, 2020, 2021, 2025]].T.round(2)
```
```output
fecha         2010  2011  2012  2015  2019   2020  2021  2025
nominal       0.54  0.58  0.55  0.56  0.58   0.58  0.59  0.54
dv01          1.50  1.63  1.55  1.59  1.66   1.64  1.67  1.52
volatilidad   3.21  4.99  3.88  1.70  1.62  13.17  2.27  1.41
min_varianza   NaN  2.07  3.56  1.60  1.29   2.29  2.24  1.18
```

La razón está en la pata corta. Con el tipo a 2 años cerca de cero, el banco central lo ancla y apenas se mueve:

```python
vol_pb = Y.diff().mul(100).rolling(252).std().resample("YE").last()
vol_pb.index = vol_pb.index.year
vol_pb.loc[[2011, 2013, 2015, 2018, 2020, 2021, 2023, 2025],
           ["TPA", "TPC"]].T.round(1)
```
```output
fecha    2011  2013  2015  2018  2020  2021  2023  2025
mercado
TPA       3.8   2.9   5.7   6.6   3.6   2.8   9.7   7.1
TPC       6.3   5.9   6.7   6.6   7.2   5.4   9.2   6.6
```

En 2011 el rendimiento a 2 años se movió 3,8 pb diarios y el de 10 años, 6,3; en 2021, 2,8 frente a 5,4. A la vez, la
duración del contrato a 2 años es un tercio de la del de 10: su precio se mueve muy poco y su volatilidad de 63
sesiones cae aún más en los meses de tipos anclados. El ratio por volatilidad compensa pidiendo 3, 5 o 13 contratos a
2 años por cada uno a 10 (3,21 a fin de 2010, 4,99 a fin de 2011, 13,17 a fin de 2020). Si después el tipo corto
vuelve a moverse, como en 2023 (9,7 pb diarios en el modelo), ese exceso de contratos convierte el spread en un largo
de duración varias veces mayor que el que se quería. El DV01 no depende de cuánto se movió el precio: es una
sensibilidad, y por eso no salta. La mínima varianza tiene el mismo problema, algo amortiguado por su ventana de 252
sesiones (3,56 a fin de 2012).

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

El ratio por volatilidad y el de mínima varianza miran hacia atrás: suponen que las patas se moverán como en la
ventana de estimación. En un cambio de régimen (tipos que salen del suelo, una pata que deja de ser líquida), la
cobertura que mejor funcionó es la que más falla. En spreads de tipos, el DV01 es la referencia; la volatilidad, un
control.

</aside>

## Contratos enteros y reequilibrio

Un ratio de 1,52 no se puede comprar. Hay que redondear, y el error depende del tamaño:

```python
def enteros(nombre, maximo=6):
    corto, largo = PATAS[nombre]
    d_c, d_l = D[corto].iloc[-1], D[largo].iloc[-1]
    filas = []
    for n_l in range(1, maximo + 1):
        n_c = round(n_l * d_l / d_c)
        resto = n_c * d_c - n_l * d_l
        filas.append({"largos": n_l, "cortos": n_c,
                      "dv01_resto": resto,
                      "resto_pct": 100 * resto / (n_l * d_l)})
    return pd.DataFrame(filas).set_index("largos")

enteros("2s10s").round(1)
```
```output
        cortos  dv01_resto  resto_pct
largos
1            2        20.6       32.0
2            3        -1.3       -1.0
3            5        19.3       10.0
4            6        -2.6       -1.0
5            8        18.0        5.6
6            9        -3.9       -1.0
```

*Fig. — DV01 que queda sin cubrir al redondear a contratos enteros el ratio del 31 de diciembre de 2025 (1,52 en el 2s10s, 2,09 en el NOB), según el número de contratos de la pata larga.* Dos líneas con puntos de 1 a 8 contratos. En el 2s10s, con un contrato a 10 años y dos a 2 años sobra un 32 % de DV01; con dos y tres, destacado, falta un 1 %; después alterna entre un 10 % y un 1 % y baja. En el NOB, discontinuo, el error se queda en torno al 4 % con cualquier tamaño hasta 5 contratos y baja al 1,9 % con 8.

Con un contrato a 10 años y dos a 2 años sobra un 32 % de DV01 en la pata corta: 20,6 USD por pb netos, un tercio
de un contrato a 10 años apostado a que bajen los tipos. Con dos y tres, el error es del 1 %. El tamaño mínimo
razonable no es «un spread»: es el número de contratos que deja el resto por debajo de lo que se tolera. En el NOB,
con $h = 2{,}09$, el redondeo a 2 por contrato deja un 4 % de 1 a 5 contratos largos; hacen falta 6 o más para que
la parte decimal del ratio cuente. Cuando existen contratos más pequeños (los micro de índices o de crudo, una décima
parte del original), sirven para ajustar el resto.

El ratio también hay que mantenerlo. Recalcularlo cada sesión cuesta comisiones; dejarlo fijo entre rolls deja que el
DV01 se desalinee:

```python
corto, largo = PATAS["2s10s"]
h_diario = R["2s10s"]["dv01"].shift(1)
roll = C[largo] != C[largo].shift(1)
h_roll = R["2s10s"]["dv01"].where(roll).ffill().shift(1)
desfase = (h_roll * D[corto] - D[largo]) / D[largo]
print(f"DV01 sin cubrir con el ratio fijo entre rolls: "
      f"mediana {100 * desfase.abs().median():.1f} %, "
      f"p95 {100 * desfase.abs().quantile(0.95):.1f} %")
for nombre, h in {"diario": h_diario, "en el roll": h_roll}.items():
    pnl = h * usd[corto] - usd[largo]
    nivel = Y[largo].diff() * 100
    pend = (Y[largo] - Y[corto]).diff() * 100
    m = pd.concat([pnl, nivel, pend], axis=1).loc["2016":].dropna()
    X = np.c_[np.ones(len(m)), m.iloc[:, 1], m.iloc[:, 2]]
    b = np.linalg.lstsq(X, m.iloc[:, 0], rcond=None)[0]
    print(f"{nombre:>10}: beta al nivel {b[1]:5.1f} USD/pb, "
          f"a la pendiente {b[2]:5.1f}")
```
```output
DV01 sin cubrir con el ratio fijo entre rolls: mediana 3.5 %, p95 8.8 %
    diario: beta al nivel   1.1 USD/pb, a la pendiente  69.5
en el roll: beta al nivel   3.6 USD/pb, a la pendiente  66.9
```

Con el ratio fijado el día del roll y sin tocar durante el trimestre, el DV01 sin cubrir es del 3,5 % en la mediana y
del 8,8 % en el 5 % de los peores días; la beta al nivel desde 2016 sube de 1,1 a 3,6 USD por pb. Es un desfase
pequeño frente al del redondeo de una cuenta con un solo spread, y lleva a una regla práctica: reequilibrar en el roll
y cuando el desfase pase de un umbral (por ejemplo, medio contrato de la pata corta), no cada día.

## Índices y materias primas: beta y unidades

Fuera de los tipos no hay DV01, pero la pregunta sigue en pie. En un spread entre índices como NQ/ES (lección
3.2.3), lo que se quiere cancelar suele ser el mercado: el Nasdaq ficticio `RVB` carga 1,15 en el factor común y el
S&P ficticio `RVA`, 1. La función `continuo` reconstruye el contrato que se tiene de cada mercado con la regla de roll
de `futuros3_contratos.csv`:

```python
ctr = pd.read_csv("futuros3_contratos.csv",
                  parse_dates=["fecha_roll"])

def continuo(cod):
    """Contrato que se tiene (roll en `fecha_roll`), su precio y su
    cambio de precio con el mismo contrato."""
    fechas = pd.DatetimeIndex(
        cur.loc[cur["mercado"] == cod, "fecha"].unique()).sort_values()
    cm = ctr[ctr["mercado"] == cod].sort_values("fecha_roll")
    i = np.searchsorted(cm["fecha_roll"], fechas, side="right")
    tiene = pd.Series(cm["contrato"].to_numpy()[i], index=fechas)
    precio = px.reindex(list(zip(fechas, tiene))).to_numpy()
    return pd.DataFrame({"precio": precio,
                         "dP": cambio_mismo_contrato(tiene)},
                        index=fechas)

A, B = continuo("RVA"), continuo("RVB")
ra = A["dP"] / A["precio"].shift(1)
rb = B["dP"] / B["precio"].shift(1)
nom = (B["precio"] * mult["RVB"]) / (A["precio"] * mult["RVA"])
h_ind = pd.DataFrame({
    "nominal": nom,
    "beta": nom * rb.rolling(252).cov(ra) / ra.rolling(252).var(),
    "volatilidad": nom * rb.rolling(63).std() / ra.rolling(63).std(),
})
nominal_b = B["precio"].shift(1) * mult["RVB"]
filas = {}
for k in h_ind:
    cob = h_ind[k].shift(1) * A["dP"] * mult["RVA"]
    pnl = (B["dP"] * mult["RVB"] - cob) / nominal_b
    m = pd.concat([pnl, ra], axis=1).loc["2011":].dropna()
    filas[k] = {
        "ratio_medio": h_ind[k].loc["2011":].mean(),
        "beta_resto": np.polyfit(m.iloc[:, 1], m.iloc[:, 0], 1)[0],
        "vol_anual_pct": 100 * m.iloc[:, 0].std() * np.sqrt(252)}
vol_b = 100 * rb.loc["2011":].std() * np.sqrt(252)
print(f"Vol. anual de RVB sola: {vol_b:.1f} %")
pd.DataFrame(filas).T.round(3)
```
```output
Vol. anual de RVB sola: 24.0 %
             ratio_medio  beta_resto  vol_anual_pct
nominal            0.903       0.151         10.486
beta               1.035       0.007         10.123
volatilidad        1.198      -0.122         10.730
```

Las tres coberturas bajan la volatilidad del 24 % de `RVB` sola a algo más del 10 %, y las tres se parecen en riesgo
total. La diferencia está en la exposición al mercado. Con el mismo nominal, el spread sigue largo un 15 % del índice
de referencia (beta 0,151): es la respuesta conocida, la beta de 1,15 menos uno. La cobertura por beta lo deja en
0,007. La de volatilidad se pasa, −0,122: iguala $\sigma_B/\sigma_A$, que es la beta dividida por la correlación
($\beta = \rho\,\sigma_B/\sigma_A$, con $\rho \approx 0{,}91$ en los datos), así que vende más índice del necesario. Es
el mismo error del 2s10s en pequeño: la volatilidad iguala riesgos, no exposiciones.

En materias primas aparece un quinto criterio: las **unidades**. El crack spread de la gasolina mide el margen de un
refinador, que se cuenta por barril:

```python
G, P = continuo("PEG"), continuo("PEC")
barriles = mult["PEG"] / 42 / mult["PEC"]     # 42 galones por barril
valor = (G["precio"] * mult["PEG"]) / (P["precio"] * mult["PEC"])
rg = G["dP"] / G["precio"].shift(1)
rc = P["dP"] / P["precio"].shift(1)
vol = valor * rg.rolling(63).std() / rc.rolling(63).std()
cr = pd.DataFrame({"valor": valor, "volatilidad": vol}).loc["2011":]
print(f"Barriles: {barriles:.2f} PEC por PEG, siempre")
cr.describe().loc[["mean", "min", "50%", "max"]].round(2)
```
```output
Barriles: 1.00 PEC por PEG, siempre
      valor  volatilidad
mean   1.26         1.43
min    0.91         0.96
50%    1.25         1.42
max    1.63         2.02
```

Un contrato de gasolina de los datos (42.000 galones) son 1.000 barriles, igual que uno de crudo: el crack en barriles
es 1:1 y no cambia nunca. Igualar el valor pide entre 0,91 y 1,63 contratos de crudo por cada uno de gasolina (1,26 de
media), porque la gasolina suele valer más por barril que el crudo; igualar el riesgo, todavía más (1,43 de media). Son
spreads distintos: el de barriles es el margen de refino, lo que cubre una refinería y lo que estudia la lección 3.2.2;
el de valor, el rendimiento relativo de los dos precios (el spread en logaritmos que usa la lección 3.2.6); el de
volatilidad, una apuesta de riesgo igualado sin significado físico.

## Errores típicos

- **Ratio de precios en vez de ratio de valores.** Dividir 112,55 entre 104,27 no da nada útil: hay que multiplicar
  cada precio por su multiplicador (2.000 en `TPA`, 1.000 en `TPC`). Con divisas distintas, convertir antes a la misma
  moneda (lección 3.3.3).
- **El ratio de hoy en el P&L de hoy.** El ratio se calcula al cierre y se aplica desde la sesión siguiente. Usar el de
  la misma sesión es anticipación (lección 2.5.1).
- **Regresión en niveles de precio.** Una regresión del precio de una pata sobre el de otra da una pendiente sin
  sentido si los precios no cointegran (lección 2.1.5). La de mínima varianza se hace en cambios de valor, en dinero.
- **El resto del redondeo.** Un spread de 1 contra 2 con un ratio de 1,52 lleva un tercio de su riesgo en tipos sin
  saberlo. Se mide el DV01 neto de la cartera, no el de un spread ideal.
- **Confiar en la volatilidad reciente de una pata anclada.** Tipos cerca de cero, una divisa con banda, un contrato
  ilíquido: la volatilidad baja no es ausencia de riesgo, es riesgo aplazado.

## Ejercicios

1. Monta el spread 5s10s (`TPB` frente a `TPC`, el que en CBOT se lista como FYT) y repite la tabla de betas de
   2016-2025 con los cuatro ratios. ¿Cuál deja la beta al nivel en cero? ¿Por qué aquí el de volatilidad se parece más
   al de DV01 que en el 2s10s?

**Solución.**

```python
PATAS["5s10s"] = ("TPB", "TPC")
R["5s10s"] = ratios("TPB", "TPC")
print(R["5s10s"].iloc[-1].round(2).to_string())
pd.DataFrame({r: betas("5s10s", r, "2016", "2025")
              for r in R["5s10s"]}).T.round(1)
```
```output
nominal         1.03
dv01            1.49
volatilidad     1.50
min_varianza    1.43
              beta_sola  beta_nivel  beta_pend  desv_dia
nominal            24.0        21.6       49.2     191.5
dv01                3.8         0.4       68.8     120.4
volatilidad         1.9        -1.3       62.8     115.8
min_varianza        3.4         0.2       64.8     118.5
```

Con DV01 (1,49 contratos de `TPB` por cada `TPC`), la beta al nivel es 0,4 USD por pb y la beta a la pendiente, 68,8,
el DV01 de `TPC`: la respuesta conocida otra vez. Aquí el ratio por volatilidad (1,50) casi coincide con el de DV01 y
deja una beta al nivel de −1,3, y el de mínima varianza, 0,2. La razón es que el 5 años no está anclado como el 2
años: con tipos cerca de cero sigue moviéndose casi tanto como el 10 años en puntos básicos, y entonces igualar riesgo
y sensibilidad son casi lo mismo. El nominal (1,03, porque los dos contratos tienen el mismo multiplicador) vuelve a
dejar una apuesta de duración: 21,6 USD por pb.

2. `TPE` es un futuro sobre un bono a 10 años en evros (QEV), la otra divisa del nivel. Calcula su P&L diario en USD
   con el tipo de cambio de `fx3_diario.parquet` y cúbrelo con `TPC` por DV01 (con el DV01 de `TPE` pasado a USD).
   ¿Cuánta varianza quita la cobertura desde 2016? Corrige el ratio con la beta de los cambios del rendimiento de
   `TPE` sobre los de `TPC`, estimada en 2010-2015, y compara.

**Solución.**

```python
fx = pd.read_parquet("fx3_diario.parquet")
x = fx[fx["divisa"] == "QEV"].set_index("fecha")["usd_por_unidad"]
x = x.reindex(D.index).ffill()
usd_e = cambio_mismo_contrato(C["TPE"]) * mult["TPE"] * x.shift(1)
dy = Y[["TPE", "TPC"]].diff().loc[:"2015"]
beta_y = dy.cov().loc["TPE", "TPC"] / dy["TPC"].var()
h_dv = (D["TPE"] * x / D["TPC"]).shift(1)
for nombre, h in {"DV01": h_dv, "DV01 x beta": h_dv * beta_y}.items():
    cub = usd_e - h * usd["TPC"]
    m = pd.concat([usd_e, cub], axis=1).loc["2016":].dropna()
    quita = 1 - m.iloc[:, 1].var() / m.iloc[:, 0].var()
    print(f"{nombre:>12}: ratio final {h.iloc[-1]:.2f}, "
          f"varianza que quita {100 * quita:.0f} %")
print(f"Beta de los rendimientos (2010-2015): {beta_y:.2f}")
```
```output
        DV01: ratio final 1.97, varianza que quita 16 %
 DV01 x beta: ratio final 1.06, varianza que quita 49 %
Beta de los rendimientos (2010-2015): 0.54
```

Pasado a USD, el DV01 de `TPE` pide 1,97 contratos de `TPC` por cada uno, y esa cobertura solo quita el 16 % de la
varianza desde 2016. El motivo: el DV01 supone que los dos rendimientos se mueven lo mismo en puntos básicos, y el
bono de Vesperia se mueve, de media, 0,54 pb por cada pb del de EE. UU. (estimado en 2010-2015, antes del periodo en el
que se prueba). Con el DV01 multiplicado por esa beta (1,06 contratos), la cobertura quita el 49 %, casi el máximo que
permite una correlación de 0,70 entre los cambios de los dos rendimientos ($\rho^2 \approx 0{,}49$). Entre curvas
distintas, el DV01 es necesario pero no suficiente: falta la beta de los tipos, que se estima y cambia (y el riesgo de
divisa, que la lección 3.3.3 trata aparte).

3. Una cuenta quiere un spread NQ/ES neutral al mercado con unos 300.000 USD de nominal en la pata de `RVB`. Con los
   datos de 2025-12-31, ¿cuántos contratos de cada pata necesita? ¿Y si usa los micro (`RVAM`, `RVBM`, una décima
   parte del multiplicador)? ¿Qué resto de beta queda en cada caso?

**Solución.**

```python
pB, pA = B["precio"].iloc[-1], A["precio"].iloc[-1]
beta_fin = h_ind["beta"].iloc[-1] / nom.iloc[-1]
print(f"Nominal de un contrato: RVB {pB * mult['RVB']:,.0f} USD, "
      f"RVA {pA * mult['RVA']:,.0f} USD; beta {beta_fin:.2f}")
for etiqueta, k in {"normales": 1, "micro": 10}.items():
    nom_b, nom_a = pB * mult["RVB"] / k, pA * mult["RVA"] / k
    n_b = max(1, round(300_000 / nom_b))
    objetivo = n_b * nom_b * beta_fin    # nominal de RVA que cubre
    n_a = round(objetivo / nom_a)
    resto = (objetivo - n_a * nom_a) / objetivo
    print(f"{etiqueta:>8}: {n_b} RVB contra {n_a} RVA, "
          f"beta sin cubrir {100 * resto:+.1f} %")
```
```output
Nominal de un contrato: RVB 437,815 USD, RVA 337,350 USD; beta 1.17
normales: 1 RVB contra 2 RVA, beta sin cubrir -31.7 %
   micro: 7 RVB contra 11 RVA, beta sin cubrir -3.5 %
```

Un contrato de `RVB` tiene casi 438.000 USD de nominal: no se puede hacer un spread de 300.000 con contratos normales.
Con 1 contrato de `RVB` y 2 de `RVA` (la beta de 1,17 pide 1,52), la cuenta vende un 31,7 % más de mercado del que
cubre: un corto de unos 160.000 USD de índice. Con los micro (una décima parte), 7 contratos de `RVBM` (306.000 USD)
contra 11 de `RVAM` dejan un −3,5 %. Los micro no cambian el ratio, cambian la resolución: lo mismo que pasaba con el
número de contratos en el 2s10s.

## Autoevaluación

1. Un empinador 2s10s se monta con 0,54 contratos a 2 años por cada contrato a 10 años, el ratio por nominal. ¿Qué exposición le queda?

   - A. Ninguna: el nominal está igualado
   - B. Una posición neta en tipos: el contrato a 10 años se mueve mucho más por punto básico, así que el spread gana o pierde con un desplazamiento paralelo casi tanto como con la pendiente
   - C. Solo riesgo de divisa
   - D. Solo el riesgo del roll

   *Respuesta B: Una posición neta en tipos: el contrato a 10 años se mueve mucho más por punto básico, así que el spread gana o pierde con un desplazamiento paralelo casi tanto como con la pendiente.* Igualar el nominal no iguala la sensibilidad a los tipos. En los datos, la cobertura por nominal deja una beta al nivel de unos 45 USD por punto básico y por contrato en los dos periodos, más de la mitad del DV01 de la pata larga (unos 70): media apuesta de duración.

2. A finales de 2020 el ratio por volatilidad del 2s10s vale 13,2 y el de DV01, 1,64. ¿Qué ha pasado?

   - A. Que el DV01 está mal calculado
   - B. Que el tipo a 2 años, cerca de cero, casi no se mueve: su volatilidad se hunde y el ratio por volatilidad pide muchos más contratos, que dejan un largo en tipos en cuanto el 2 años vuelve a moverse
   - C. Que el 10 años tiene más DV01 que nunca
   - D. Que el spread está en contango

   *Respuesta B: Que el tipo a 2 años, cerca de cero, casi no se mueve: su volatilidad se hunde y el ratio por volatilidad pide muchos más contratos, que dejan un largo en tipos en cuanto el 2 años vuelve a moverse.* El ratio por volatilidad iguala el dinero en riesgo de las dos patas con lo que se movieron en los últimos 63 días. Cuando una pata está anclada (tipos cerca de cero), su volatilidad tiende a cero y el ratio explota. En 2010-2015, con esa cobertura, el empinador tuvo una beta al nivel de −33 USD por pb: un largo en duración con forma de spread.

3. El ratio por DV01 del 2s10s es 1,52 y una cuenta pequeña opera 1 contrato a 10 años contra 2 a 2 años. ¿Qué tiene?

   - A. Un 32 % de DV01 de más en la pata corta: con 2 contratos a 10 años y 3 a 2 años el error baja al 1 %
   - B. Un spread casi perfecto
   - C. Un spread neutral en volatilidad
   - D. Un 50 % de DV01 de menos

   *Respuesta A: Un 32 % de DV01 de más en la pata corta: con 2 contratos a 10 años y 3 a 2 años el error baja al 1 %.* 2 × 42,55 = 85,1 USD por pb frente a 64,47 de la pata larga: sobran 20,6 USD por pb, un 32 %. El redondeo de un ratio no entero solo se arregla con más contratos o con contratos más pequeños, si existen.

4. En NQ/ES, un spread igualado en nominal deja una beta de 0,15 al índice de referencia. ¿Qué ratio la anula?

   - A. El de nominal
   - B. El de volatilidad: 1,20 contratos de RVA por cada uno de RVB de media
   - C. El de beta: el nominal multiplicado por la beta del Nasdaq ficticio sobre el S&P ficticio, 1,15 en los datos
   - D. Ninguno

   *Respuesta C: El de beta: el nominal multiplicado por la beta del Nasdaq ficticio sobre el S&P ficticio, 1,15 en los datos.* El índice tecnológico de los datos carga 1,15 en el factor de mercado: con el mismo nominal, el spread sigue largo un 15 % de mercado. El de volatilidad se pasa (beta −0,12), porque la correlación no es 1: σ_B / σ_A = β / ρ.

5. Un refinador quiere cubrir su margen de gasolina. ¿Qué ratio usa?

   - A. El de volatilidad
   - B. El de valor nominal
   - C. El de mínima varianza, siempre
   - D. El de unidades: un contrato de gasolina (42.000 galones, 1.000 barriles) por cada contrato de crudo (1.000 barriles), porque su margen se mide por barril

   *Respuesta D: El de unidades: un contrato de gasolina (42.000 galones, 1.000 barriles) por cada contrato de crudo (1.000 barriles), porque su margen se mide por barril.* El margen de refino es el precio de los productos menos el del crudo, por barril. Cubrirlo exige el mismo número de barriles en cada pata. Igualar el valor (1,26 contratos de crudo por cada uno de gasolina de media en los datos) o el riesgo es otra apuesta: la del rendimiento relativo de los dos precios.

## Ideas clave

- El ratio de cobertura es el número de contratos de una pata por contrato de la otra, y decide qué riesgo queda: no hay un ratio «correcto» sin decir qué se quiere neutralizar.
- Por nominal se iguala el dinero; por DV01, la sensibilidad a un punto básico; por volatilidad, el dinero en riesgo reciente; por mínima varianza, el riesgo total estimado. En el 2s10s de los datos, a fin de 2025, dan 0,54, 1,52, 1,41 y 1,18.
- Solo la cobertura por DV01 deja la beta al nivel de tipos en cero en los dos periodos (−0,2 y 1,1 USD por pb) y la beta a la pendiente igual al DV01 de la pata larga: la respuesta conocida.
- El ratio por volatilidad y el de mínima varianza dependen de lo que se movió hace poco: con el tipo a 2 años anclado cerca de cero llegan a 3, 5 y hasta 13 contratos, y convierten el spread en una apuesta de duración.
- Redondear importa: 1 contra 2 deja un 32 % de DV01 sin cubrir; 2 contra 3, un 1 %. Reequilibrar solo en el roll deja un 3,5 % de desfase mediano.
- En índices, el ratio que neutraliza el mercado es el de beta (NQ/ES: 1,15 veces el nominal); en materias primas, el de unidades cuando lo que se opera es un margen físico.

## Recursos

### Research

- [Impacto del factor estacional sobre los bonos americanos](https://pythonparatrading.com/estacionalidad-usa-yields/) · Research-0017 · 3 min

### Documentación

- [CME Group Client Systems Wiki: Implied Orders (spreads intercommodity de CBOT con patas en proporción mayor que uno: NOB, FYT, TUB)](https://cmegroupclientsite.atlassian.net/wiki/spaces/EPICSANDBOX/pages/457095346/Implied+Orders) · cmegroupclientsite.atlassian.net
- [NumPy: numpy.linalg.lstsq (mínimos cuadrados)](https://numpy.org/doc/stable/reference/generated/numpy.linalg.lstsq.html) · numpy.org

### Libros

- [Robert Carver, «Systematic Trading» (Harriman House): escalado por volatilidad](https://www.harriman-house.com/systematic-trading) · harriman-house.com

### Datos

- [Datos del nivel 3: DV01 de los futuros de tipos (CSV)](https://pythonparatrading.com/curso/datos/nivel-3/tipos3_dv01.csv) · Descarga · tipos3_dv01.csv
- [Datos del nivel 3: especificaciones de los futuros (CSV)](https://pythonparatrading.com/curso/datos/nivel-3/futuros3_especificaciones.csv) · Descarga · futuros3_especificaciones.csv

Lección anterior: [3.2.3 · Spreads de tipos y de índices: 2s10s, NOB, NQ/ES y Europa frente a EE. UU.](https://pythonparatrading.com/curso-python-trading/nivel-3/spreads-de-tipos-y-de-indices/)

Lección siguiente: [3.2.5 · Datos y backtesting de spreads: cotizados, sintéticos y por patas](https://pythonparatrading.com/curso-python-trading/nivel-3/datos-y-backtesting-de-spreads-por-patas/)
