---
title: "3.2.2 Spreads intercommodity: crack, spark y crush"
description: "Spreads entre mercados: crack, spark, crush, oro/plata y trigo/maíz. Unidades, ratios de contratos, riesgo y reversión medida frente a la plantada."
url: "https://pythonparatrading.com/curso-python-trading/nivel-3/spreads-intercommodity-crack-spark-y-crush/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-3/spreads-intercommodity-crack-spark-y-crush.png"
---

# 3.2.2 · Spreads intercommodity: crack, spark y crush

> Una refinería compra crudo y vende gasolina; una central compra gas y vende electricidad; una molturadora compra soja y vende harina y aceite. Sus márgenes son spreads entre dos o tres futuros distintos, con unidades distintas y un vínculo físico que los ata. Esta lección los construye con el mismo mes de entrega en todas las patas, mide su riesgo y comprueba si su reversión es la que se plantó en los datos.

- 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: 65 min
- Requisitos: [3.2.1 · Spreads de calendario, butterflies y condors](https://pythonparatrading.com/curso-python-trading/nivel-3/spreads-de-calendario-butterflies-y-condors/), [2.1.5 · Cointegración: Engle-Granger, Johansen y vida media de la reversión](https://pythonparatrading.com/curso-python-trading/nivel-2/cointegracion-engle-granger-johansen-y-half-life/)

## Al terminar esta lección

- Construir los spreads de proceso (crack 1:1 y 3:2:1, spark, crush) y los de valor relativo (oro/plata, trigo/maíz) convirtiendo unidades y con el mismo mes de entrega en todas las patas.
- Pasar de la fórmula de un spread al número entero de contratos de cada pata (10:11:9 en el crush, 1:1 en el crack) y medir el error de redondeo.
- Medir el riesgo de un spread frente al de sus patas y relacionarlo con la correlación entre ellas.
- Estimar la vida media de la reversión, compararla con la respuesta conocida y reconocer el falso positivo de un ratio sin vínculo físico.

## Márgenes de proceso y valor relativo

Un spread de calendario (lección 3.2.1) enfrenta dos meses del mismo producto. Un **spread intercommodity** enfrenta
productos distintos. Los más operados son márgenes de una industria: lo que gana quien transforma un producto en
otro. Una refinería compra crudo y vende gasolina y gasóleo (el **crack**); una central de ciclo combinado compra gas
y vende electricidad (el **spark**); una molturadora compra soja y vende harina y aceite (el **crush**).

```mermaid
flowchart LR
  C[Crudo] --> R[Refinería]
  R --> G[Gasolina]
  R --> D[Gasóleo]
  S[Soja] --> M[Molturadora]
  M --> H[Harina]
  M --> A[Aceite]
  N[Gas] --> E[Central]
  E --> L[Electricidad]
```

El vínculo físico es lo que hace interesantes estos spreads. Si el margen de refino sube mucho, las refinerías
procesan más crudo y venden más gasolina, y el margen baja; si cae por debajo del coste de operar, paran unidades y
el margen se recupera. El spread no puede alejarse sin límite de lo que cuesta transformar el producto, al menos a
plazos de meses. Otros spreads intercommodity no tienen ese ancla: el **ratio oro/plata** o el **trigo frente al
maíz** son apuestas de valor relativo entre productos que se sustituyen en parte o que se han movido juntos en el
pasado.

```mermaid
flowchart TB
  P[Dos mercados] --> V{¿Vínculo físico?}
  V -- sí --> T[Margen de proceso]
  V -- no --> R[Valor relativo]
  T --> RV[Reversión posible]
  R --> SA[Sin ancla]
```

Los datos del nivel 3 tienen los tres márgenes de proceso y dos spreads de valor relativo, con la **respuesta
conocida** en `spreads3_verdad.csv`: qué componente del modelo mueve cada spread, a qué velocidad revierte y cuáles
son controles. Son datos sintéticos: los mercados imitan al WTI, la gasolina RBOB, el gasóleo de Nueva York, el gas
Henry Hub, la electricidad de punta de PJM, el complejo de la soja, el trigo, el maíz, el oro y la plata, pero los
precios no son de mercado.

## Unidades y ratios de contratos

Antes de restar dos precios hay que ponerlos en la misma unidad. Cada contrato cotiza en la suya:

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

cur = pd.read_parquet("futuros3_curvas.parquet")
ctr = pd.read_csv("futuros3_contratos.csv",
                  parse_dates=["vencimiento", "fecha_roll"])
esp = pd.read_csv("futuros3_especificaciones.csv",
                  index_col="mercado")
mercados = ["PEC", "PEG", "PEH", "GNA", "ELB", "GRS", "GRH",
            "GRA", "GRT", "GRM", "MTO", "MTP"]
esp.loc[mercados, ["imita", "multiplicador", "unidad_cotizacion",
                   "meses"]]
```
```output
                                imita  multiplicador   unidad_cotizacion         meses
mercado
PEC                WTI Crude Oil (CL)         1000.0          USD/barril  FGHJKMNQUVXZ
PEG                RBOB Gasoline (RB)        42000.0           USD/galón  FGHJKMNQUVXZ
PEH               NY Harbor ULSD (HO)        42000.0           USD/galón  FGHJKMNQUVXZ
GNA        Henry Hub Natural Gas (NG)        10000.0           USD/MMBtu  FGHJKMNQUVXZ
ELB      PJM Western Hub Peak mensual          800.0             USD/MWh  FGHJKMNQUVXZ
GRS                     Soybeans (ZS)           50.0     centavos/bushel       FHKNQUX
GRH                 Soybean Meal (ZM)          100.0  USD/tonelada corta      FHKNQUVZ
GRA                  Soybean Oil (ZL)          600.0      centavos/libra      FHKNQUVZ
GRT            Chicago SRW Wheat (ZW)           50.0     centavos/bushel         HKNUZ
GRM                         Corn (ZC)           50.0     centavos/bushel         HKNUZ
MTO                         Gold (GC)          100.0            USD/onza        GJMQVZ
MTP                       Silver (SI)         5000.0            USD/onza         HKNUZ
```

Las conversiones que definen cada spread:

| Spread | Fórmula | Unidad | Conversión |
|---|---|---|---|
| Crack de la gasolina 1:1 | $42 \cdot \text{PEG} - \text{PEC}$ | USD/barril | 42 galones por barril |
| Crack del gasóleo 1:1 | $42 \cdot \text{PEH} - \text{PEC}$ | USD/barril | 42 galones por barril |
| Crack 3:2:1 | $(2 \cdot 42 \cdot \text{PEG} + 42 \cdot \text{PEH} - 3 \cdot \text{PEC})/3$ | USD/barril | dos de gasolina por uno de gasóleo |
| Spark | $\text{ELB} - 7 \cdot \text{GNA}$ | USD/MWh | heat rate de 7 MMBtu por MWh |
| Crush | $0{,}022 \cdot \text{GRH} + 0{,}11 \cdot \text{GRA} - \text{GRS}/100$ | USD/bushel | 0,022 t de harina y 11 lb de aceite por bushel |
| Oro/plata | $\text{MTO} / \text{MTP}$ | cociente | onzas de plata por onza de oro |
| Trigo/maíz | $\text{GRT} / \text{GRM}$ | cociente | mismos bushels |

En el **crack**, el contrato de gasolina o gasóleo tiene 42.000 galones, que son 1.000 barriles: el mismo volumen que
el de crudo. Un contrato de producto contra uno de crudo es el crack 1:1, y la bolsa lo trata así en sus ejemplos. El
**3:2:1** (tres de crudo, dos de gasolina, uno de gasóleo) imita el rendimiento de una refinería típica de EE. UU., que
produce aproximadamente el doble de gasolina que de destilados.

En el **spark**, el *heat rate* es la eficiencia de la central: los MMBtu de gas que quema para producir un MWh. La
EIA publica el spark spread con un heat rate de referencia de 7.000 Btu/kWh (7 MMBtu/MWh), el de una central de ciclo
combinado moderna y eficiente; y el spark no cuenta otros costes de la central (transporte del gas, mantenimiento,
emisiones), así que un spark positivo no es un beneficio.

En el **crush**, la guía de CME parte de los rendimientos físicos: un bushel de 60 libras da unas 44 libras de harina
y 11 de aceite (más cascarilla y merma). Con 44/2.000 = 0,022 toneladas cortas de harina por bushel y los tamaños de los
contratos, la proporción en contratos es:

```python
bushels = 10 * 5000                    # 10 contratos de soja
harina_t = 44 / 2000 * bushels         # toneladas cortas
aceite_lb = 11 * bushels               # libras
print(f"Harina: {harina_t:,.0f} t = {harina_t / 100:.2f} contratos")
print(f"Aceite: {aceite_lb:,.0f} lb = "
      f"{aceite_lb / 60000:.2f} contratos")
```
```output
Harina: 1,100 t = 11.00 contratos
Aceite: 550,000 lb = 9.17 contratos
```

*Fig. — Anatomía del crush de la soja: lo que vale lo que sale de un bushel frente a lo que cuesta (contratos de julio de 2025, sesión del 4 de junio).* Barras horizontales en USD por bushel con los contratos de julio de 2025 el 4 de junio: la harina que sale de un bushel (0,022 toneladas cortas a 340,2 USD) vale 7,48; el aceite (11 libras a 50,17 centavos), 5,52; la soja cuesta 10,89. El margen del crush, destacado, es 7,48 + 5,52 − 10,89 = 2,12 USD por bushel.

La figura hace la cuenta con los contratos de julio de 2025 el 4 de junio: la harina de un bushel vale 7,48 USD, el
aceite 5,52 y la soja cuesta 10,89, así que el margen bruto del crush es de 2,12 USD por bushel.

Diez contratos de soja dan exactamente once de harina y 9,17 de aceite. CME facilita el *board crush* como 30
contratos: 10 de soja, 11 de harina y 9 de aceite. El aceite queda un 2 % corto: con números enteros pequeños, el
ratio nunca es exacto. El segundo ejercicio hace la misma búsqueda para el spark spread.

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

Las fórmulas de la tabla usan las unidades de cotización. La soja y el aceite cotizan en centavos (por bushel y por
libra), la harina en USD por tonelada corta: de ahí el $/100$ de la soja y el 0,11 (11 libras × 0,01 USD) del aceite.
Un error de unidades en un spread no da un error pequeño: da otro spread.

</aside>

## El mismo mes de entrega en todas las patas

Un crack de julio es gasolina de julio contra crudo de julio. Mezclar meses añade a la posición un spread de
calendario que nadie ha pedido. Las patas tienen además calendarios distintos: la soja lista siete meses al año, la
harina y el aceite ocho, y solo seis son comunes (enero, marzo, mayo, julio, agosto y septiembre). El código busca,
para cada sesión, el primer mes de entrega listado en todas las patas cuya fecha de roll no haya llegado en ninguna, y
calcula el cambio diario con los contratos del día anterior, como en la lección 3.2.1:

```python
cm = cur.merge(ctr[["contrato", "mes_entrega"]], on="contrato")
px = cm.pivot_table(index="fecha", columns=["mercado", "mes_entrega"],
                    values="liquidacion")

def mes_comun(mercados, meses=None):
    """Mes de entrega que se tiene a la vez en todas las patas: el
    primero listado en todas cuya primera fecha de roll no ha
    llegado."""
    c = ctr[ctr["mercado"].isin(mercados)]
    r = c.groupby("mes_entrega").agg(
        n=("mercado", "nunique"), roll=("fecha_roll", "min"))
    r = r[r["n"] == len(mercados)]
    if meses:
        r = r[[int(m[5:]) in meses for m in r.index]]
    roll = r["roll"][::-1].cummin()[::-1]
    pos = np.searchsorted(roll.to_numpy(), px.index.to_numpy(),
                          side="right")
    pos = np.minimum(pos, len(r) - 1)
    return pd.Series(r.index.to_numpy()[pos], index=px.index)

def patas_mes(mercados, meses=None):
    """Precio de hoy de cada pata en el mes común de hoy y cambio
    de hoy de cada pata en el mes común de ayer (sin saltos)."""
    mes = mes_comun(mercados, meses)
    dif = px.diff()
    def toma(tabla, m, meses_):
        idx = pd.MultiIndex.from_arrays([mes.index, meses_.to_numpy()])
        return tabla[m].stack().reindex(idx).to_numpy()
    nivel = pd.DataFrame({m: toma(px, m, mes) for m in mercados},
                         index=mes.index)
    cambio = pd.DataFrame({m: toma(dif, m, mes.shift(1))
                           for m in mercados}, index=mes.index)
    return nivel, cambio, mes

niv, dif, mes = patas_mes(["PEG", "PEC"])
mes.loc["2025-06-02":"2025-06-06"]
```
```output
fecha
2025-06-02    2025-07
2025-06-03    2025-07
2025-06-04    2025-07
2025-06-05    2025-07
2025-06-06    2025-07
dtype: str
```

El 4 de junio de 2025, el crack que se tiene es el de julio en las dos patas. Con esa función se construyen los seis
spreads en sus unidades:

```python
SPREADS = {
    "crack_gasolina": (["PEG", "PEC"], None,
                       lambda p: 42 * p.PEG - p.PEC),
    "crack_gasoleo": (["PEH", "PEC"], None,
                      lambda p: 42 * p.PEH - p.PEC),
    "spark": (["ELB", "GNA"], None, lambda p: p.ELB - 7 * p.GNA),
    "crush": (["GRS", "GRH", "GRA"], [1, 3, 5, 7, 8, 9],
              lambda p: 0.022 * p.GRH + 0.11 * p.GRA - p.GRS / 100),
    "trigo_maiz": (["GRT", "GRM"], [3, 5, 7, 9, 12],
                   lambda p: p.GRT / p.GRM),
    "aceite_harina": (["GRA", "GRH"], [1, 3, 5, 7, 8, 9, 10, 12],
                      lambda p: p.GRA / p.GRH),
}
UNIDAD = {"crack_gasolina": "USD/barril", "crack_gasoleo": "USD/barril",
          "spark": "USD/MWh", "crush": "USD/bushel",
          "trigo_maiz": "cociente", "aceite_harina": "cociente"}
niveles = {}
for nombre, (ms, meses, f) in SPREADS.items():
    niveles[nombre] = f(patas_mes(ms, meses)[0])
tabla = pd.DataFrame(niveles).describe().T
tabla["unidad"] = pd.Series(UNIDAD)
tabla[["unidad", "mean", "min", "max"]].round(3)
```
```output
                    unidad    mean    min     max
crack_gasolina  USD/barril  21.318 -8.886  65.059
crack_gasoleo   USD/barril  31.515  9.177  78.124
spark              USD/MWh  15.711 -2.250  74.091
crush           USD/bushel   1.034 -0.289   3.611
trigo_maiz        cociente   1.355  1.019   1.779
aceite_harina     cociente   0.132  0.087   0.251
```

El crack medio de la gasolina sintética es de 21,3 USD por barril y llega a ser negativo (−8,89); el del gasóleo, más
alto y más estable, nunca baja de 9,18. El spark también llega a ser negativo (−2,25 USD/MWh): hay días en que quemar
gas para producir electricidad de punta no cubre ni el combustible. El crush medio es de 1,03 USD por bushel. La figura
de la cabecera muestra los dos cracks: el de la gasolina oscila mucho más, con un ciclo anual muy marcado. Antes de
interpretarlo, conviene mirar el nivel medio por mes de entrega:

```python
mes_gas = patas_mes(["PEG", "PEC"])[2]
nivel_mes = niveles["crack_gasolina"].groupby(mes_gas.str[5:]).mean()
nivel_mes.round(1).to_frame("usd_barril").T
```
```output
              01    02    03    04    05    06    07    08    09    10   11    12
usd_barril  13.6  14.9  23.1  31.1  33.8  32.8  29.5  26.7  19.2  11.0  9.3  11.1
```

El crack de la gasolina de entrega en mayo vale de media 33,8 USD; el de noviembre, 9,3. La gasolina de verano es otro
producto (más cara de producir) y la de invierno se vende menos, así que la curva de la gasolina tiene una forma
estacional fija. El ciclo de la cabecera es, sobre todo, el paso de un mes de entrega al siguiente.

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

Que el crack de mayo cotice más caro que el de noviembre no quiere decir que comprar el crack en noviembre y venderlo
en mayo gane dinero: el contrato de mayo ya cotiza caro el día que se lista. La forma estacional de la curva está
descontada (en los datos, por construcción). Lo que sí podría ganar dinero es una **deriva** estacional, que el
contrato que se mantiene suba de forma predecible en ciertos meses. Separar una cosa de la otra, y no engañarse con
16 años de datos, es el tema de la lección 3.2.6.

</aside>

## Riesgo: depende de la correlación de las patas

Un spread de dos patas con volatilidades $\sigma_a$ y $\sigma_b$ y correlación $\rho$, con pesos iguales en
logaritmos, tiene una volatilidad de

$$
\sigma_{\text{spread}} = \sqrt{\sigma_a^2 + \sigma_b^2 - 2 \rho \, \sigma_a \sigma_b}.
$$

Cuanto más correladas están las patas, menos riesgo queda. Medido con los mismos contratos de un día a otro:

```python
LOG = {
    "crack_gasolina": lambda p: np.log(42 * p.PEG / p.PEC),
    "crack_gasoleo": lambda p: np.log(42 * p.PEH / p.PEC),
    "spark": lambda p: np.log(p.ELB / (7 * p.GNA)),
    "crush": lambda p: np.log((0.022 * p.GRH + 0.11 * p.GRA)
                              / (p.GRS / 100)),
    "trigo_maiz": lambda p: np.log(p.GRT / p.GRM),
    "aceite_harina": lambda p: np.log(p.GRA / p.GRH),
}
filas = {}
for nombre, (ms, meses, _) in SPREADS.items():
    nivel, cambio, mes = patas_mes(ms, meses)
    antes = nivel.shift(1)            # ayer, contrato de ayer
    r = np.log((antes + cambio) / antes).dropna()     # mismo contrato
    s = LOG[nombre](antes + cambio) - LOG[nombre](antes)
    a, b = ms[0], ms[-1]
    filas[nombre] = {"pata_a": a, "vol_a": r[a].std() * 252 ** 0.5,
                     "pata_b": b, "vol_b": r[b].std() * 252 ** 0.5,
                     "correlacion": r[a].corr(r[b]),
                     "vol_spread": s.std() * 252 ** 0.5}
riesgo = pd.DataFrame(filas).T
num = ["vol_a", "vol_b", "correlacion", "vol_spread"]
riesgo[num] = riesgo[num].astype(float).round(2)
riesgo
```
```output
               pata_a  vol_a pata_b  vol_b  correlacion  vol_spread
crack_gasolina    PEG   0.39    PEC   0.36         0.91        0.16
crack_gasoleo     PEH   0.38    PEC   0.36         0.93        0.14
spark             ELB   0.48    GNA   0.42         0.81        0.29
crush             GRS   0.19    GRA   0.27         0.71        0.10
trigo_maiz        GRT   0.29    GRM   0.24         0.83        0.17
aceite_harina     GRA   0.28    GRH   0.25         0.49        0.27
```

Las patas del crack se mueven con una correlación de 0,91 (gasolina) y 0,93 (gasóleo): el spread conserva un 14-16 %
de volatilidad anual frente al 36-39 % de cada pata. El spark, con 0,81, conserva un 29 %; el aceite frente a la
harina, con 0,49, un 27 %: casi tanto como cada pata. En el crush la tabla enseña la soja y el aceite (0,71), pero el
spread incluye las tres patas, y su volatilidad es la menor de todas, un 10 %. Un spread intercommodity tiene más
riesgo que uno de calendario del mismo mercado (lección 3.2.1), porque dos productos distintos se parecen menos que
dos meses del mismo.

Ese riesgo menor también se refleja en el margen, aunque menos que en los calendarios. CME explica que, por la
compensación entre las patas, un crack puede requerir hasta un 30 % menos de capital que una posición simple en
gasolina o gasóleo, y en su ejemplo con micros el crack pide 480 USD frente a una pata de gasóleo que, sola, pide 599:
un 19 % menos. Un calendario de crudo de los datos pide el 12 % del margen simple, no el 70-80 %.

## Reversión: lo plantado frente a lo medido

El vínculo físico sugiere que los márgenes de proceso revierten. En los datos lo hacen por construcción: el componente
propio de cada spread es un proceso de Ornstein-Uhlenbeck con velocidad $\kappa_P$, y su vida media en sesiones es
$\ln 2 / \kappa_P \cdot 252$. La pregunta útil es si un método estándar la recupera. El método es el de la lección
2.1.5: un AR(1) del nivel del log-spread, al que antes se le quita su media por mes de entrega (la forma estacional de
la curva del apartado anterior), y un contraste ADF de raíz unitaria:

$$
\Delta x_t = a + b \, x_{t-1} + \varepsilon_t, \qquad \text{vida media} = \frac{\ln 2}{-b}.
$$

```python
from statsmodels.tsa.stattools import adfuller

ver = pd.read_csv("spreads3_verdad.csv", index_col="spread")

def adf_p(x):
    """p-valor del ADF; result_object=False conserva la tupla de
    siempre y evita el FutureWarning de statsmodels 0.15."""
    return adfuller(x, result_object=False)[1]

def vida_media(nivel, clave):
    """AR(1) del nivel menos su media por clave (mes de entrega)."""
    x = (nivel - nivel.groupby(clave).transform("mean")).dropna()
    b = np.polyfit(x.shift(1).dropna(), x.diff().dropna(), 1)[0]
    return np.log(2) / -b if b < 0 else np.inf

res = {}
for nombre, (ms, meses, _) in SPREADS.items():
    nivel, _, mes = patas_mes(ms, meses)
    s = LOG[nombre](nivel)
    clave = mes.str[5:]
    x = (s - s.groupby(clave).transform("mean")).dropna()
    res[nombre] = {"plantada": ver.loc[nombre, "vida_media_sesiones"],
                   "medida": vida_media(s, clave),
                   "p_adf": adf_p(x),
                   "control": ver.loc[nombre, "control"]}
pd.DataFrame(res).T.astype({"plantada": float, "medida": float,
                            "p_adf": float}).round(
    {"plantada": 1, "medida": 1, "p_adf": 4})
```
```output
                plantada  medida   p_adf control
crack_gasolina      43.7    36.6  0.0000   False
crack_gasoleo       58.2    40.5  0.0000    True
spark               29.1    17.5  0.0000   False
crush               69.9    54.6  0.0000   False
trigo_maiz         174.7    71.4  0.0003   False
aceite_harina      218.3   157.6  0.0522    True
```

*Fig. — Vida media de la reversión plantada en los datos frente a la medida con un AR(1), seis spreads y el control oro/plata.* Dispersión logarítmica. Todos los spreads quedan por debajo de la diagonal: la vida media medida es menor que la plantada (spark 17,5 frente a 29,1 sesiones; crack de la gasolina 36,6 frente a 43,7; crush 54,6 frente a 69,9; trigo/maíz 71,4 frente a 174,7; aceite/harina 157,6 frente a 218,3). Destacado, el ratio oro/plata, un paseo aleatorio sin reversión (vida media infinita), da una vida media medida de 698 sesiones.

El ADF rechaza la raíz unitaria en cinco de los seis spreads y el AR(1) recupera el orden de magnitud de la reversión,
pero con un sesgo sistemático: todas las vidas medias medidas son más cortas que las plantadas. En el spark, 17,5
sesiones frente a 29,1; en el crack de la gasolina, 36,6 frente a 43,7; en el trigo frente al maíz, 71,4 frente a
174,7, menos de la mitad. Es el sesgo del estimador autorregresivo en muestras finitas que la lección 2.1.5 midió con
simulaciones, al que se suma que cada spread mezcla su componente propio con restos de los factores comunes de las
patas.

El aceite frente a la harina es el caso difícil: revierte (vida media plantada de 218 sesiones, casi un año), pero tan
despacio que el ADF se queda en p = 0,052 con 16 años de datos. Un contraste al 5 % lo daría por no estacionario.

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

La columna `control` de `spreads3_verdad.csv` se refiere a la **estacionalidad** (lección 3.2.6): el crack del gasóleo
y el aceite frente a la harina no tienen deriva estacional plantada, pero sí revierten. El control de la reversión es
el ratio oro/plata.

</aside>

## Un ratio sin ancla: oro frente a plata

El ratio oro/plata es el ejemplo clásico de valor relativo: cuántas onzas de plata compra una onza de oro. En los
datos es, por construcción, un paseo aleatorio con episodios de deriva: no hay nada que lo devuelva a una media.

```python
def simple(mercado):
    c = cur[cur["mercado"] == mercado].merge(ctr, on=["mercado",
                                                     "contrato"])
    c = c[c["fecha"] < c["fecha_roll"]].sort_values(
        ["fecha", "vencimiento"])
    return c.groupby("fecha")["liquidacion"].first()

ratio = simple("MTO") / simple("MTP")
lr = np.log(ratio)
uno = pd.Series("", index=lr.index)
print(f"Ratio oro/plata: de {ratio.min():.1f} a {ratio.max():.1f}")
print(f"Toda la muestra: vida media {vida_media(lr, uno):.0f} "
      f"sesiones, p ADF {adf_p(lr):.2f}")
for ini in range(2010, 2023, 3):
    t = lr.loc[str(ini):str(ini + 2)]
    print(f"{ini}-{ini + 2}: vida media "
          f"{vida_media(t, uno.loc[t.index]):4.0f}, "
          f"p ADF {adf_p(t):.2f}")
```
```output
Ratio oro/plata: de 39.5 a 108.2
Toda la muestra: vida media 698 sesiones, p ADF 0.58
2010-2012: vida media  183, p ADF 0.37
2013-2015: vida media   61, p ADF 0.11
2016-2018: vida media  153, p ADF 0.84
2019-2021: vida media  136, p ADF 0.66
2022-2024: vida media  111, p ADF 0.51
```

El ratio va de 39,5 a 108,2 en 16 años. El AR(1) de toda la muestra da una vida media de 698 sesiones (casi tres
años), que suena a «reversión lenta»; en ventanas de tres años da entre 61 y 183 sesiones, que suena a reversión
operable. Pero el ADF no rechaza la raíz unitaria ni en toda la muestra (p = 0,58) ni en ninguna ventana (de 0,11 a
0,84). La regresión del cambio sobre el nivel casi siempre da un coeficiente negativo en una muestra finita, aunque el
proceso no revierta: la vida media sale finita porque el estimador está sesgado, no porque haya reversión. Es el punto
destacado de la figura anterior.

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

Una estrategia de reversión sobre un ratio sin vínculo físico se calibra con una vida media que el propio estimador
fabrica. En una ventana de tres años, el ratio oro/plata «revierte» con una vida media de 61 sesiones; la ventana
siguiente da 153 y nada garantiza que el ratio vuelva. Antes de operar la reversión de un spread hay que contrastarla
(ADF, KPSS, lección 2.1.2) y, si es posible, explicar qué la produce.

</aside>

## Lo que esto cambia en un sistema

- **Unidades, primero.** Cada pata en su unidad de cotización, convertida antes de restar; un error de unidades da
  otro spread.
- **El mismo mes en todas las patas.** Si no, el spread lleva dentro un calendario. Con calendarios de entrega
  distintos, solo valen los meses comunes.
- **Ratios enteros.** La fórmula da fracciones; los contratos, no. 10:11:9 en el crush, 25:14 en el spark: con pocos
  contratos, el redondeo deja exposición a una pata. Las coberturas por nominal, DV01 o volatilidad son el tema de la
  lección 3.2.4.
- **Reversión contrastada.** La vida media medida es más corta que la real, y un ratio sin ancla también tiene una
  vida media medida. La reversión se contrasta y se explica.
- **Forma estacional no es deriva.** El nivel por mes de entrega está descontado; la deriva, si existe, se estudia
  aparte (lección 3.2.6). El backtest por patas, con sus costes y el riesgo de ejecutar una pata sin la otra, es el de
  la lección 3.2.5.

## Ejercicios

1. Construye el P&L diario en USD del crack 3:2:1 (dos contratos de gasolina y uno de gasóleo contra tres de crudo) y
   comprueba que es la suma de dos cracks de gasolina y uno de gasóleo. Compara su riesgo en 2025 con la suma de los
   riesgos de los tres cracks por separado.

**Solución.**

```python
nivel, cambio, mes = patas_mes(["PEG", "PEH", "PEC"])
usd = 1000 * (2 * 42 * cambio.PEG + 42 * cambio.PEH
              - 3 * cambio.PEC)                 # 2 PEG + 1 PEH − 3 PEC
gas = 1000 * (42 * cambio.PEG - cambio.PEC)     # crack 1:1
die = 1000 * (42 * cambio.PEH - cambio.PEC)
print("Igual a 2 cracks de gasolina + 1 de gasóleo:",
      np.allclose(usd.dropna(), (2 * gas + die).dropna()))
r25 = pd.DataFrame({"usd": usd, "gas": gas, "die": die}).loc["2025"]
print(f"Riesgo diario del 3:2:1: {r25['usd'].std():,.0f} USD")
print(f"Suma de los riesgos por separado: "
      f"{2 * r25['gas'].std() + r25['die'].std():,.0f} USD")
print(f"Correlación de los dos cracks: {gas.corr(die):.2f}")
```
```output
Igual a 2 cracks de gasolina + 1 de gasóleo: True
Riesgo diario del 3:2:1: 1,982 USD
Suma de los riesgos por separado: 2,529 USD
Correlación de los dos cracks: 0.18
```

El 3:2:1 es, exactamente, dos cracks de gasolina más uno de gasóleo. Su riesgo diario en 2025 (1.982 USD) es menor que
la suma de los riesgos por separado (2.529 USD) porque los dos cracks están poco correlados entre sí (0,18): sus
componentes propios son independientes en el modelo de los datos, y solo comparten el crudo, que se cancela en los
dos. Combinar márgenes de productos distintos diversifica.

2. Con los tamaños de los contratos de electricidad (800 MWh) y gas (10.000 MMBtu) de la ficha y un heat rate de 7,
   busca el ratio entero de contratos que replica el spark spread con menos error, con hasta 25 contratos de
   electricidad.

**Solución.**

```python
mwh = esp.loc["ELB", "multiplicador"]          # MWh por contrato
mmbtu = esp.loc["GNA", "multiplicador"]        # MMBtu por contrato
exacto = 7 * mwh / mmbtu                       # GNA por ELB
print(f"Contratos de gas por contrato de electricidad: {exacto:.2f}")
mejor = []
for n_elb in range(1, 26):
    n_gna = round(n_elb * exacto)
    if n_gna:
        error = n_gna / n_elb / exacto - 1
        mejor.append((abs(error), n_elb, n_gna, error))
for _, a, b, e in sorted(mejor)[:4]:
    print(f"{a:2d} ELB : {b:2d} GNA  error {e:+.1%}")
```
```output
Contratos de gas por contrato de electricidad: 0.56
25 ELB : 14 GNA  error +0.0%
16 ELB :  9 GNA  error +0.4%
 9 ELB :  5 GNA  error -0.8%
18 ELB : 10 GNA  error -0.8%
```

Un contrato de electricidad de 800 MWh quema 5.600 MMBtu de gas con un heat rate de 7: 0,56 contratos de gas. El
ratio exacto con números pequeños es 25 contratos de electricidad por 14 de gas; 16:9 deja un error del 0,4 % y 9:5,
del 0,8 %. Con menos contratos el error crece: con 2:1, el gas se queda un 11 % corto. En una posición pequeña, el spark spread
«exacto» no existe, y lo que sobra es una posición simple en gas.

3. Una molturadora cubre su margen vendiendo el board crush (vende harina y aceite, compra soja). Explica qué riesgo
   cubre, qué riesgo no cubre y por qué el resultado depende del mes de entrega que elija.

**Solución.**

Vender el crush fija hoy el margen de los meses que elija: si después la harina y el aceite bajan respecto a la soja,
lo que pierde en el negocio físico lo gana en los futuros. Cubre el precio de los tres productos en Chicago, no su
propio rendimiento (si sus plantas sacan menos harina por bushel que 0,022 toneladas, el margen real es otro), ni la
base local (la diferencia entre el precio de su zona y el del contrato), ni los costes de operar, que no están en la
fórmula. El mes importa porque solo seis meses son comunes a las tres patas y porque cada mes de entrega tiene su
propio nivel de crush: la forma de la curva de cada producto (cosecha vieja frente a nueva) está descontada en cada
contrato. La cobertura del mes de julio no fija el margen de octubre.

## Autoevaluación

1. ¿Por qué un crack de gasolina 1:1 se hace con un contrato de gasolina por cada contrato de crudo?

   - A. Porque los dos cotizan en USD
   - B. Porque el contrato de gasolina (42.000 galones) equivale a 1.000 barriles, el mismo volumen que el de crudo
   - C. Porque la refinería produce la misma cantidad de gasolina que de crudo
   - D. Porque la bolsa obliga a operar las patas en la misma cantidad

   *Respuesta B: Porque el contrato de gasolina (42.000 galones) equivale a 1.000 barriles, el mismo volumen que el de crudo.* Un barril tiene 42 galones: 42.000 galones son 1.000 barriles, el tamaño del contrato de crudo. Por eso el crack en USD por barril es 42 × precio del producto − precio del crudo, y un contrato de cada lado lo replica sin redondeos.

2. El board crush de CME se negocia como 10 contratos de soja, 11 de harina y 9 de aceite. ¿De dónde sale esa proporción?

   - A. De la liquidez de cada contrato
   - B. De los rendimientos físicos: 50.000 bushels dan 1.100 toneladas cortas de harina (11 contratos de 100) y 550.000 libras de aceite (9,17 contratos de 60.000, redondeado a 9)
   - C. De igualar el nominal de las tres patas
   - D. Es una convención sin relación con el proceso

   *Respuesta B: De los rendimientos físicos: 50.000 bushels dan 1.100 toneladas cortas de harina (11 contratos de 100) y 550.000 libras de aceite (9,17 contratos de 60.000, redondeado a 9).* Un bushel de 60 libras da unas 44 libras de harina y 11 de aceite (guía del crush de CME). Con contratos de 5.000 bushels, 100 toneladas cortas y 60.000 libras, la proporción entera más cercana con pocos contratos es 10:11:9; el aceite queda un 2 % corto.

3. En los datos, la vida media medida de todos los spreads con reversión es menor que la plantada (por ejemplo, 17,5 frente a 29,1 sesiones en el spark). ¿Por qué?

   - A. Porque los datos tienen errores
   - B. Por el sesgo del estimador AR(1) en muestras finitas: el coeficiente sale menor de lo real y la reversión parece más rápida (lección 2.1.5)
   - C. Porque la reversión se acelera con el tiempo
   - D. Porque las vidas medias plantadas están en días naturales

   *Respuesta B: Por el sesgo del estimador AR(1) en muestras finitas: el coeficiente sale menor de lo real y la reversión parece más rápida (lección 2.1.5).* El estimador por MCO del coeficiente autorregresivo está sesgado hacia abajo, más cuanto más lenta es la reversión respecto a la muestra. Además, la vida media medida mezcla el componente que revierte con otros factores del spread. Lo importante es el signo del error: un backtest calibrado con la vida media medida espera una reversión más rápida que la real.

4. El ratio oro/plata de los datos da una vida media de 698 sesiones en toda la muestra y de 61 a 183 en ventanas de tres años. ¿Qué se concluye?

   - A. Que revierte lentamente y se puede operar a plazos largos
   - B. Que revierte deprisa en ventanas cortas
   - C. Nada a favor de la reversión: es un paseo aleatorio por construcción, el ADF no rechaza (p = 0,58) y un AR(1) estimado siempre da una vida media finita
   - D. Que hay que usar ventanas más cortas

   *Respuesta C: Nada a favor de la reversión: es un paseo aleatorio por construcción, el ADF no rechaza (p = 0,58) y un AR(1) estimado siempre da una vida media finita.* La regresión del cambio sobre el nivel casi siempre da un coeficiente negativo en una muestra finita, aunque el proceso no revierta: la vida media sale finita por construcción del estimador. El contraste (ADF) es el que dice si hay reversión; aquí, en ninguna ventana rechaza al 5 %.

5. El crack de la gasolina de los datos vale de media 33,8 USD por barril en los contratos de mayo y 9,3 en los de noviembre. ¿Es una oportunidad estacional?

   - A. Sí: comprar el crack en noviembre y venderlo en mayo
   - B. No necesariamente: es la forma estacional de la curva (la gasolina de verano es otro producto y cotiza más cara), y el mercado ya la descuenta en cada contrato
   - C. Sí, porque la diferencia es de más de 20 USD
   - D. No, porque el crack de la gasolina no tiene estacionalidad

   *Respuesta B: No necesariamente: es la forma estacional de la curva (la gasolina de verano es otro producto y cotiza más cara), y el mercado ya la descuenta en cada contrato.* El nivel medio por mes de entrega es una propiedad de cada contrato, conocida desde que se lista. Ganar con ella exigiría que el contrato de mayo subiera mientras se mantiene, y eso es una deriva, otra cosa. Si existe y si es operable lo estudia la lección 3.2.6.

## Ideas clave

- Un spread intercommodity enfrenta mercados distintos: los de proceso (crack, spark, crush) tienen un vínculo físico; los de valor relativo (oro/plata, trigo/maíz), solo de sustitución o de historia.
- Antes de restar hay que convertir unidades (42 galones por barril, heat rate de 7 MMBtu por MWh, 0,022 toneladas de harina y 11 libras de aceite por bushel) y usar el mismo mes de entrega en todas las patas.
- El ratio de contratos sale de los tamaños: 1:1 en el crack, 10:11:9 en el crush, 25:14 en el spark de los datos; los ratios pequeños redondean.
- El riesgo del spread depende de la correlación de sus patas: un 16 % anual en el crack de la gasolina (patas con 0,91 de correlación) frente a un 27 % en aceite/harina (0,49).
- La vida media medida es menor que la plantada en todos los spreads que revierten; el control oro/plata, un paseo aleatorio, da una vida media finita y un ADF que no rechaza.

## Recursos

### Documentación

- [CME Group: Trading Crack Spreads (crack 1:1 y 3:2:1, conversión a barriles, compensación de márgenes)](https://www.cmegroup.com/articles/2024/trading-crack-spreads.html) · cmegroup.com
- [CME Group: An Introduction to Crack Spreads (ratios 1:1, 3:2:1, 5:3:2 y 2:1:1)](https://www.cmegroup.com/articles/whitepapers/an-introduction-to-crack-spreads.html) · cmegroup.com
- [CME Group: Soybean Crush Reference Guide (rendimientos por bushel y board crush)](https://www.cmegroup.com/education/files/soybean-crush-reference-guide.pdf) · cmegroup.com
- [CME Group: Soybean Crush Spreads (10 soja, 11 harina, 9 aceite)](https://www.cmegroup.com/trading/agricultural/grain-and-oilseed/soybean-crush-spreads.html) · cmegroup.com
- [U.S. EIA: An introduction to spark spreads (2013)](https://www.eia.gov/todayinenergy/detail.php?id=9911) · eia.gov
- [U.S. EIA: Today in Energy, Daily Prices (spark spread con heat rate de referencia de 7.000 Btu/kWh)](https://www.eia.gov/todayinenergy/prices.php) · eia.gov

### Datos

- [Datos del nivel 3: verdad de los spreads (parámetros plantados y controles)](https://pythonparatrading.com/curso/datos/nivel-3/spreads3_verdad.csv) · Descarga · spreads3_verdad.csv

Lección anterior: [3.2.1 · Spreads de calendario, butterflies y condors](https://pythonparatrading.com/curso-python-trading/nivel-3/spreads-de-calendario-butterflies-y-condors/)

Lección siguiente: [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/)
