Lección 3.2.4 · 60 min

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.

Fig. — Empinador 2s10s de los datos: contratos de la nota a 2 años por cada contrato de la nota a 10 años con tres ratios de cobertura, al cierre de cada mes de 2010 a 2025
Lecciones del nivel 3
  1. 3.1 · Futuros en profundidad

    1. 3.1.1 Contratos de futuros principales
    2. 3.1.2 Curva de futuros: base, cost of carry, contango y roll yield
    3. 3.1.3 Gestión del roll, márgenes, SPAN y apalancamiento implícito
    4. 3.1.4 Sistemas de futuros diversificados: el enfoque de Carver
  2. 3.2 · Spreads de futuros

    1. 3.2.1 Spreads de calendario, butterflies y condors
    2. 3.2.2 Spreads intercommodity: crack, spark y crush
    3. 3.2.3 Spreads de tipos y de índices: 2s10s, NOB, NQ/ES y Europa frente a EE. UU.
    4. 3.2.4 Ratios de cobertura: por nominal, por DV01 y por volatilidad
    5. 3.2.5 Datos y backtesting de spreads: cotizados, sintéticos y por patas
    6. 3.2.6 Estacionalidad de spreads: análisis multi-año, patrones y trampas
  3. 3.3 · Forex avanzado

    1. 3.3.1 Microestructura FX: OTC, ECN, last look y sesiones
    2. 3.3.2 Carry y riesgo de crash, momentum y value en divisas; cestas
    3. 3.3.3 Riesgo de divisa en carteras multi-moneda; ejecución vía OANDA, IBKR o MT5
  4. 3.4 · Teoría y valoración de opciones

    1. 3.4.1 Payoffs, paridad put-call y límites de arbitraje
    2. 3.4.2 Árboles binomiales, Black-Scholes-Merton, Black-76 y Monte Carlo
    3. 3.4.3 Griegas: delta, gamma, vega, theta, rho, vanna, volga y charm
    4. 3.4.4 Volatilidad implícita: Newton-Raphson, Brent, py_vollib y QuantLib
    5. 3.4.5 Sonrisa, skew y estructura temporal; superficie de volatilidad con SVI
    6. 3.4.6 Volatilidad realizada frente a implícita; prima de riesgo de volatilidad; VIX y VSTOXX
    7. 3.4.7 Modelos avanzados: Heston, SABR y volatilidad local
  5. 3.5 · Estructuras de opciones en Python

    1. 3.5.1 Estructuras direccionales y verticales: call, put, covered call, collar y spreads
    2. 3.5.2 Estructuras de volatilidad y rango: straddles, strangles, cóndores y mariposas
    3. 3.5.3 Calendarios, diagonales, ratios, backspreads, risk reversals y jade lizard
    4. 3.5.4 Herramienta propia: payoff, griegas agregadas y escenarios
    5. 3.5.5 Gestión: ajustes, roll, cierre anticipado, asignación temprana y pin risk
  6. 3.6 · Estrategias sistemáticas de volatilidad y opciones

    1. 3.6.1 Venta sistemática de prima: put-write, strangles y su riesgo de cola
    2. 3.6.2 Delta hedging, gamma scalping y dispersión
    3. 3.6.3 Estrategias con VIX y VSTOXX: futuros y ETP de volatilidad
    4. 3.6.4 Backtesting de opciones: datos, fills y ajustes por eventos
  7. 3.7 · Cripto derivados y estrategias específicas

    1. 3.7.1 Perpetuos: mecánica del funding, mark price y liquidaciones
    2. 3.7.2 Arbitraje de funding, basis con futuros y arbitraje entre exchanges
    3. 3.7.3 Opciones cripto, datos on-chain y DeFi
    4. 3.7.4 ccxt para operar en varios exchanges
    5. 3.7.5 MiCA: el marco de la UE para criptoactivos y sus proveedores (CASP)

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 hh (contratos de una pata por cada contrato de la otra), decide qué riesgo se cancela y cuál queda. Con FF el precio, mm el multiplicador del contrato (lo que vale un punto de precio), DV01\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 LL con contratos de la pata CC, son:

hnominal=FL mLFC mC,hDV01=DV01LDV01C,hσ=σL FL mLσC FC mC,hMV=Cov⁡(ΔVL,ΔVC)Var⁡(ΔVC)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 ΔV\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).

RatioQué igualaQué necesitaCuándo tiene sentido
NominalDinero de cada pataPrecio y multiplicadorRendimiento relativo de dos activos parecidos
DV01Sensibilidad a 1 pbDV01 por contratoSpreads de tipos: aislar la pendiente
VolatilidadDinero en riesgo recienteσ estimadaPatas sin relación estructural
Mínima varianzaRiesgo total pasadoRegresión de P&LReducir riesgo, sin decir cuál
UnidadesCantidad físicaFicha del contratoMárgenes físicos: crack, crush, spark

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

tiposmercadomargen físicoriesgo total¿Qué cancelas?DV01BetaUnidadesVolatilidad o MV
Fuente 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:

IN [01]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)
OUT [01]DV01 por contrato de los futuros de tipos
        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:

IN [02]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)
OUT [02]Cambio de precio con el mismo contrato
              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:

IN [03]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)
OUT [03]Cuatro ratios a 31 de diciembre de 2025
              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.

Lo que deja cada cobertura

Un empinador (steepener) gana si la curva se empina: compra hh contratos del plazo corto y vende uno del largo. Su P&L diario por contrato largo es h ΔVC−ΔVLh\,\Delta V_C - \Delta V_L, con hh 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:

P&Lt=a+βnivel ΔytL+βpend Δ(ytL−ytC)+εt\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 βnivel≈0\beta_{\text{nivel}} \approx 0, y βpend\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.

IN [04]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)
OUT [04]Qué deja cada cobertura: nivel y pendiente
                             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

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:

IN [05]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)
OUT [05]El ratio año a año
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:

IN [06]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)
OUT [06]La pata corta del 2s10s cerca de tipos cero
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).

Contratos enteros y reequilibrio

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

IN [07]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)
OUT [07]Contratos enteros
        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

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,09h = 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:

IN [08]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}")
OUT [08]Reequilibrar cada día o solo en el roll
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:

IN [09]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)
OUT [09]NQ/ES: nominal, beta y volatilidad
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 σB/σA\sigma_B/\sigma_A, que es la beta dividida por la correlación (β=ρ σB/σA\beta = \rho\,\sigma_B/\sigma_A, con ρ≈0,91\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:

IN [10]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)
OUT [10]Crack de la gasolina: barriles, valor y volatilidad
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
IN [11]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)
OUT [11]El 5s10s con los cuatro ratios
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.

  1. 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
IN [12]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}")
OUT [12]Cobertura entre dos curvas
        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 (ρ2≈0,49\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).

  1. 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
IN [13]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} %")
OUT [13]NQ/ES con contratos enteros y micro
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.

Respuesta plegada en cada pregunta

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?

    • ANinguna: el nominal está igualado
    • BUna 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
    • CSolo riesgo de divisa
    • DSolo el riesgo del roll
    Ver respuesta

    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?

    • AQue el DV01 está mal calculado
    • BQue 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
    • CQue el 10 años tiene más DV01 que nunca
    • DQue el spread está en contango
    Ver respuesta

    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?

    • AUn 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 %
    • BUn spread casi perfecto
    • CUn spread neutral en volatilidad
    • DUn 50 % de DV01 de menos
    Ver respuesta

    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?

    • AEl de nominal
    • BEl de volatilidad: 1,20 contratos de RVA por cada uno de RVB de media
    • CEl de beta: el nominal multiplicado por la beta del Nasdaq ficticio sobre el S&P ficticio, 1,15 en los datos
    • DNinguno
    Ver respuesta

    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?

    • AEl de volatilidad
    • BEl de valor nominal
    • CEl de mínima varianza, siempre
    • DEl 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
    Ver respuesta

    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

Datos