Lección 3.1.4 · 75 min

Sistemas de futuros diversificados: el enfoque de Carver

Robert Carver propone un sistema de futuros que no compra ni vende por señales sino por previsiones: números continuos, escalados para que su media en valor absoluto sea 10, combinados entre reglas y convertidos en contratos con el riesgo objetivo de la cuenta. Esta lección lo construye entero sobre los 23 futuros sintéticos del nivel, con tendencia y carry, y mide cada pieza: los escalares, los multiplicadores de diversificación, el buffering, los costes y lo que aporta combinar.

Fig. — Un sistema al estilo de Carver en 23 futuros sintéticos: cada regla sola y la combinación, netas de costes, con un 20 % de volatilidad objetivo (2011-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)

Previsiones, no señales

Los sistemas de las lecciones 2.2.2 y 2.3.1 decidían «largo, corto o fuera» y dimensionaban por volatilidad. Robert Carver, en Systematic Trading (2015) y Advanced Futures Trading Strategies (2023), separa esas dos cosas de otra manera: cada regla produce una previsión (forecast), un número continuo que dice cuánto se espera y con cuánta convicción, y una única fórmula convierte cualquier previsión en contratos. Las piezas:

Precio ajustadoRegla brutaVolatilidad× escalarTope ±20Pesos × FDMContratosBuffer y orden
Fuente Mermaid
flowchart TB
  P[Precio ajustado] --> R[Regla bruta]
  V[Volatilidad] --> R
  R --> E[× escalar]
  E --> T[Tope ±20]
  T --> C[Pesos × FDM]
  C --> N[Contratos]
  N --> B[Buffer y orden]

Las convenciones son las de su librería abierta, pysystemtrade (licencia GPL v3): la previsión media en valor absoluto es 10 (average_absolute_forecast: 10.0 en su configuración por defecto), se recorta a ±20 (forecast_cap: 20.0), y los multiplicadores de diversificación tienen un tope de 2,5 (dm_max: 2.5). Con una previsión de +10 el sistema tiene la posición «media» de un mercado; con +20, el doble; con −5, la mitad en corto. El objetivo de esta lección es construir el sistema entero y medir qué aporta cada pieza, no reproducir pysystemtrade línea a línea: donde la lección se separa de su configuración, se dice.

Los datos de partida: Carver calcula las reglas de tendencia sobre el precio ajustado por diferencia (el «Panamá» de la lección 1.4.6): el cambio diario en puntos del contrato que se tiene, acumulado. Conserva las diferencias de precio, que es lo que gana o pierde un contrato, y la volatilidad se mide en las mismas unidades:

IN [01]python
import numpy as np
import pandas as pd

curvas = pd.read_parquet("futuros3_curvas.parquet")
ctr = pd.read_csv("futuros3_contratos.csv",
                  parse_dates=["vencimiento", "fecha_roll"])
esp = pd.read_csv("futuros3_especificaciones.csv",
                  index_col="mercado")
fx = pd.read_parquet("fx3_diario.parquet")
usd_qev = fx[fx["divisa"] == "QEV"].set_index("fecha")[
    "usd_por_unidad"]
MERCADOS = list(esp.index[esp["mercado_base"].isna()])

def serie(mercado):
    """Contrato que se tiene (roll en fecha_roll), su precio, el
    cambio diario en puntos del mismo contrato y el carry anual."""
    c = curvas[curvas["mercado"] == mercado]
    P = c.pivot(index="fecha", columns="contrato",
                values="liquidacion")
    cm = ctr[ctr["mercado"] == mercado].reset_index(drop=True)
    pos = np.searchsorted(cm["fecha_roll"].to_numpy(),
                          P.index.to_numpy(), side="right")
    tiene = cm["contrato"].to_numpy()[pos]
    sig = cm["contrato"].to_numpy()[np.minimum(pos + 1, len(cm) - 1)]
    i, j = P.columns.get_indexer(tiene), P.columns.get_indexer(sig)
    f, Pv = np.arange(len(P)), P.to_numpy()
    ayer = np.r_[i[0], i[:-1]]
    dif = np.r_[np.nan, Pv[f[1:], ayer[1:]] - Pv[f[1:] - 1, ayer[1:]]]
    v = cm.set_index("contrato")["vencimiento"]
    dt = (v.reindex(sig).to_numpy()
          - v.reindex(tiene).to_numpy()).astype("timedelta64[D]")
    F2 = np.where(j >= 0, Pv[f, np.maximum(j, 0)], np.nan)
    carry = np.log(Pv[f, i] / F2) / (dt.astype(float) / 365)
    return pd.DataFrame({"contrato": tiene, "precio": Pv[f, i],
                         "dif": dif, "carry": carry}, index=P.index)

S = {m: serie(m) for m in MERCADOS}
fechas = S["RVA"].index
tabla = lambda col: pd.DataFrame(
    {m: S[m][col] for m in MERCADOS}).reindex(fechas)
precio, dif, contrato = tabla("precio"), tabla("dif"), tabla("contrato")
carry = tabla("carry")
ajustado = dif.fillna(0).cumsum()          # ajuste por diferencia
sigma_p = dif.ewm(span=35, min_periods=20).std()   # puntos por día
print(precio.shape, fechas[0].date(), fechas[-1].date())
pd.DataFrame({"precio": precio.iloc[-1],
              "sigma_dia_pts": sigma_p.iloc[-1],
              "sigma_anual_pct": 100 * np.sqrt(252) * sigma_p.iloc[-1]
              / precio.iloc[-1]}).loc[["RVA", "TPC", "PEC", "GRM"]
                                      ].round(3)
OUT [01]Precio del contrato que se tiene, ajustado por diferencia
(4024, 23) 2010-01-04 2025-12-31
       precio  sigma_dia_pts  sigma_anual_pct
RVA  6747.000        131.746           30.998
TPC   112.547          0.460            6.481
PEC    60.900          1.544           40.252
GRM   340.250          4.196           19.579

23 mercados y 4.024 sesiones. La volatilidad en puntos es una media exponencial de 35 sesiones de los cambios diarios, la misma ventana que usa por defecto la regla EWMAC de pysystemtrade (vol_days=35); Carver usa además variantes que mezclan la volatilidad reciente con la de largo plazo, que aquí no se aplican. El último día, RVA se mueve 131,7 puntos diarios y la nota a 10 años, 0,46: tres órdenes de magnitud que hay que quitar antes de comparar reglas entre mercados.

EWMAC: la regla y su escalado

La regla de tendencia de Carver es la diferencia entre dos medias exponenciales del precio ajustado, rápida y lenta (con la lenta cuatro veces más larga), dividida por la volatilidad diaria del precio en puntos:

EWMAC(r,l)t=EMAr(P)t−EMAl(P)tσP,t\text{EWMAC}(r, l)_t = \frac{\mathrm{EMA}_r(P)_t - \mathrm{EMA}_l(P)_t}{\sigma_{P,t}}

Es lo que hace ewmac.py en pysystemtrade: raw_ewmac = fast_ewma - slow_ewma y raw_ewmac / vol.ffill(). El resultado está en desviaciones típicas diarias y no tiene una escala natural, así que se multiplica por un escalar que deja su media absoluta en 10. El escalar se estima juntando todos los mercados, para que no dependa de uno, y con una ventana inicial que no se usa después para juzgar la regla:

IN [02]python
def ewmac(rapida, lenta):
    m_r = ajustado.ewm(span=rapida, min_periods=rapida).mean()
    m_l = ajustado.ewm(span=lenta, min_periods=lenta).mean()
    return (m_r - m_l) / sigma_p

ESTIMA = slice("2011", "2013")     # ventana para los escalares
brutos = {f"ewmac{a}_{4 * a}": ewmac(a, 4 * a)
          for a in (8, 16, 32, 64)}
escalar = {}
for k, b in brutos.items():
    media_abs = b.loc[ESTIMA].abs().stack().mean()
    resto = b.loc["2014":].abs().stack().mean()
    escalar[k] = 10 / media_abs
    print(f"{k:11s} escalar {10 / media_abs:5.2f}  "
          f"(con 2014-2025: {10 / resto:5.2f})")
OUT [02]EWMAC bruto: diferencia de medias entre la volatilidad
ewmac8_32   escalar  5.65  (con 2014-2025:  5.94)
ewmac16_64  escalar  3.74  (con 2014-2025:  4.01)
ewmac32_128 escalar  2.45  (con 2014-2025:  2.65)
ewmac64_256 escalar  1.76  (con 2014-2025:  1.76)

Los escalares estimados con 2011-2013 son 5,65, 3,74, 2,45 y 1,76 para las cuatro velocidades; con 2014-2025, 5,94, 4,01, 2,65 y 1,76. La configuración de ejemplo del capítulo 15 de pysystemtrade usa 5,3, 3,75, 2,65 y 1,87: casi los mismos, en datos completamente distintos. No es casualidad. Con la regla en unidades de riesgo, su escala depende de las ventanas de las medias y de cómo se mide la volatilidad, y poco del mercado. Por eso un escalar estimado con muchos mercados vale para todos, y por eso es razonable fijarlo una vez y no reestimarlo cada día. Lo que el escalar no dice es si la regla gana dinero.

IN [03]python
def escala(bruto, k, tope=20):
    return (bruto * k).clip(-tope, tope)

prev = {"ewmac16_64": escala(brutos["ewmac16_64"],
                             escalar["ewmac16_64"]),
        "ewmac64_256": escala(brutos["ewmac64_256"],
                              escalar["ewmac64_256"])}
for k, p in prev.items():
    x = p.loc["2011":].stack()
    print(k, f"media |f| {x.abs().mean():.1f},",
          f"en el tope {100 * (x.abs() == 20).mean():.1f} %,",
          f"desv. típica {x.std():.1f}")
OUT [03]Previsiones escaladas y recortadas
ewmac16_64 media |f| 8.9, en el tope 9.3 %, desv. típica 10.8
ewmac64_256 media |f| 9.1, en el tope 11.3 %, desv. típica 11.0

En toda la muestra, la media absoluta queda en 8,9 y 9,1, un poco por debajo de 10 porque el tope recorta los extremos: el 9,3 % y el 11,3 % de los días-mercado están en ±20. El tope existe por dos motivos: una previsión enorme suele ser un error de datos o un mercado en un movimiento que ya se ha producido, y limita lo que puede pesar un solo mercado en la cartera.

El carry como previsión

El carry de la lección 3.1.2 (la pendiente anualizada entre el contrato que se tiene y el siguiente) se convierte en previsión igual: en unidades de riesgo, dividido por la volatilidad anual del rendimiento. pysystemtrade calcula el carry bruto como «el roll anualizado dividido por la volatilidad anualizada» (rawdata.py) y lo suaviza con una media exponencial de 90 días (carry.py); aquí se suaviza con 30 sesiones, una elección de la lección:

IN [04]python
vol_anual = (dif / precio.shift(1)).ewm(span=35, min_periods=20
                                       ).std() * np.sqrt(252)
bruto_carry = carry.ewm(span=30, min_periods=10).mean() / vol_anual
escalar["carry"] = 10 / bruto_carry.loc[ESTIMA].abs().stack().mean()
prev["carry"] = escala(bruto_carry, escalar["carry"])
print(f"escalar del carry: {escalar['carry']:.1f}")
ultimo = prev["carry"].loc["2025-11-26"]
ultimo.sort_values().round(1).iloc[[0, 1, 2, -3, -2, -1]]
OUT [04]Carry anual entre la volatilidad anual
escalar del carry: 16.7
GRS   -20.0
GRH   -20.0
GRM   -20.0
GNA    15.6
PEC    19.5
PEH    20.0
Name: 2025-11-26 00:00:00, dtype: float64

El escalar del carry sale 16,7, distinto del 30 de la configuración de pysystemtrade, porque los datos y la forma de calcular el carry no son los mismos (aquí, en logaritmos y con otra suavización): el escalar se estima para cada definición. El 26 de noviembre de 2025, los agrícolas de soja y maíz están en −20 (contango fuerte) y la energía cerca de +20 (backwardation del crudo y del gasóleo, lección 3.1.2). Un detalle que el código no trata y un sistema real sí: en los mercados con forma estacional en la curva (gas, electricidad), el carry entre meses consecutivos salta con el calendario, y esa parte no es carry (lección 3.1.2).

Combinar: pesos y multiplicador de diversificación

La previsión combinada es una media ponderada de las previsiones de cada regla. Como las reglas no están perfectamente correladas, la media se mueve menos que cada una y su media absoluta queda por debajo de 10. El multiplicador de diversificación de las previsiones (FDM) la devuelve a su escala:

FDM=1w′ρ w\text{FDM} = \frac{1}{\sqrt{w'\rho\,w}}

con ww los pesos y ρ\rho la matriz de correlación de las previsiones. Los pesos de la lección son 0,25 para cada EWMAC y 0,50 para el carry, parecidos a los del ejemplo del capítulo 15 de pysystemtrade (0,21, 0,08 y 0,21 para tres EWMAC y 0,50 para el carry):

IN [05]python
apiladas = pd.DataFrame({k: p.loc[ESTIMA].stack()
                         for k, p in prev.items()}).dropna()
rho = apiladas.corr()
w = pd.Series({"ewmac16_64": 0.25, "ewmac64_256": 0.25,
               "carry": 0.50})
fdm = min(2.5, 1 / np.sqrt(w @ rho @ w))
print(rho.round(2))
print(f"FDM = {fdm:.3f}")
combinada = sum(prev[k] * w[k] for k in w.index) * fdm
combinada = combinada.clip(-20, 20)
x = combinada.loc["2011":].stack()
print(f"combinada: media |f| {x.abs().mean():.1f}, "
      f"en el tope {100 * (x.abs() == 20).mean():.1f} %")
OUT [05]Correlación de las previsiones y FDM
             ewmac16_64  ewmac64_256  carry
ewmac16_64         1.00         0.61   0.34
ewmac64_256        0.61         1.00   0.36
carry              0.34         0.36   1.00
FDM = 1.263
combinada: media |f| 7.9, en el tope 7.5 %

Las dos EWMAC tienen una correlación de 0,61 (son la misma idea a dos velocidades) y el carry, de 0,34 y 0,36 con ellas. El FDM es 1,263. La combinada tiene una media absoluta de 7,9, por debajo de 10: el FDM se estimó con 2011-2013 y el tope recorta el 7,5 % de los días. Un sistema que quisiera exactamente su riesgo objetivo reestimaría el FDM con una ventana creciente; la consecuencia, aquí, es que el sistema tendrá algo menos de volatilidad de la pedida.

Fig. — Crudo sintético, 2019-2023: precio ajustado por diferencia y previsión combinada del sistema, recortada a ±20

La figura enseña la previsión combinada del crudo de 2019 a 2023 bajo su precio ajustado. Pasa casi todo 2020 en el tope de −20 (tendencia bajista y contango a la vez), sube con la recuperación de 2021-2022 sin pasar de +11 (la tendencia es positiva pero el carry, en los datos, sigue siendo negativo buena parte de ese tiempo) y vuelve a −20 en 2023. El precio ajustado por diferencia llega a valores cercanos a cero a finales de 2023: con este ajuste, los niveles antiguos no son precios reales, y por eso las reglas usan diferencias de precio y no porcentajes sobre el ajustado.

Del forecast al número de contratos

La posición de cada mercado sale de una sola fórmula. Con un capital KK, un riesgo objetivo anual τ\tau, el peso del mercado en la cartera wiw_i y el riesgo anual en dinero de un contrato (lección 3.1.1):

Ni=fi10⋅IDM⋅wi⋅K τσP,i252⋅Mi⋅XiN_i = \frac{f_i}{10} \cdot \text{IDM} \cdot w_i \cdot \frac{K \, \tau}{\sigma_{P,i}\sqrt{252} \cdot M_i \cdot X_i}

donde MiM_i es el multiplicador y XiX_i el tipo de cambio a USD. El multiplicador de diversificación de los instrumentos (IDM) hace con los mercados lo que el FDM con las reglas: como los mercados no se mueven juntos, una cartera con un τ/n\tau / n en cada uno tiene menos riesgo que τ\tau, y el IDM lo compensa.

IN [06]python
normal = (dif / sigma_p.shift(1)).loc[ESTIMA]
rho_i = normal.corr()
w_i = pd.Series(1 / len(MERCADOS), index=MERCADOS)
idm_bruto = 1 / np.sqrt(w_i @ rho_i @ w_i)
idm = min(2.5, idm_bruto)
print(f"IDM sin tope {idm_bruto:.2f}, con tope {idm:.2f}")
OUT [06]IDM con las correlaciones de los rendimientos
IDM sin tope 2.81, con tope 2.50

Con 23 mercados, pesos iguales y las correlaciones de los datos (altas dentro de cada sector y casi nulas entre sectores, lección 3.1.1), el IDM sin tope sería 2,81; con el tope de 2,5, se queda en 2,5. El tope es una prudencia: el IDM se estima con correlaciones de calma, y en una crisis las correlaciones suben y el riesgo real de la cartera también. La configuración de ejemplo de pysystemtrade usa 1,89 con su universo. Los pesos iguales por mercado son una simplificación: Carver reparte por sectores y después dentro de cada uno, para que cinco índices correlados no pesen cinco veces más que una divisa.

IN [07]python
CAPITAL, TAU = 5_000_000, 0.20
mult = esp.loc[MERCADOS, "multiplicador"]
cambio = pd.DataFrame(
    {m: (usd_qev.reindex(fechas).ffill()
         if esp.loc[m, "divisa"] == "QEV"
         else pd.Series(1.0, index=fechas)) for m in MERCADOS})
riesgo_contrato = sigma_p * np.sqrt(252) * mult * cambio  # USD/año
media = idm * w_i * CAPITAL * TAU / riesgo_contrato
optimo = combinada / 10 * media
d = "2025-11-26"
pd.DataFrame({"prevision": combinada.loc[d],
              "riesgo_contrato": riesgo_contrato.loc[d],
              "posicion_media": media.loc[d],
              "optimo": optimo.loc[d]}).loc[
    ["RVA", "TPA", "PEC", "GNA", "MTO", "GRM"]].round(2)
OUT [07]Contratos óptimos de un mercado un día
     prevision  riesgo_contrato  posicion_media  optimo
RVA       8.44         83822.01            1.30    1.09
TPA       0.89          4835.93           22.48    1.99
PEC      20.00         20918.48            5.20   10.39
GNA      14.79         22301.88            4.87    7.21
MTO       7.27         26192.67            4.15    3.02
GRM     -17.47          2768.80           39.26  -68.58

Con 5 millones de USD y un 20 % de riesgo objetivo, la posición media (la de una previsión de +10) es de 1,30 contratos en RVA, 22,48 en la nota a 2 años y 39,26 en el maíz. El 26 de noviembre de 2025, la previsión del crudo está en el tope (+20) y el sistema querría 10,39 contratos, el doble de su posición media; la del maíz es de −17,47 y querría 68,58 contratos cortos. RVA, con +8,44, querría 1,09: con un contrato grande de índice, el redondeo pesa.

Buffering y costes

Una posición óptima de 1,09 contratos un día y de 0,96 al siguiente no justifica una operación. Carver evita esas operaciones con un buffer: se calcula una banda alrededor del óptimo (en pysystemtrade, buffer_size: 0.10, un 10 % de la posición media) y solo se opera si la posición actual queda fuera, y solo hasta el borde de la banda (buffer_trade_to_edge: True):

sínoÓptimo ± buffer¿Posición dentro?No operarOperar al borde
Fuente Mermaid
flowchart LR
  O[Óptimo ± buffer] --> Q{¿Posición dentro?}
  Q -- sí --> X[No operar]
  Q -- no --> Y[Operar al borde]

El bucle diario usa la posición decidida con los datos de la sesión anterior, que se ejecuta al cierre de la sesión (lección 2.2.2), en contratos enteros. Cada cambio paga medio tick y la comisión por contrato, y cada roll, dos lados sobre toda la posición:

IN [08]python
def contratos(optimo, media, buffer=0.10):
    """Posición entera: se negocia solo si la actual queda fuera de
    óptimo ± buffer·posición media, y solo hasta el borde."""
    P = optimo.shift(1).to_numpy()     # decidido con datos de t − 1
    B = buffer * media.shift(1).to_numpy()
    N, actual = np.zeros_like(P), np.zeros(P.shape[1])
    for t in range(len(P)):
        bajo, alto = np.round(P[t] - B[t]), np.round(P[t] + B[t])
        nuevo = np.clip(actual, bajo, alto)
        actual = np.where(np.isnan(P[t]), 0.0, nuevo)
        N[t] = actual
    return pd.DataFrame(N, index=optimo.index, columns=optimo.columns)

coste_lado = (0.5 * esp.loc[MERCADOS, "valor_tick"]
              + esp.loc[MERCADOS, "comision"]) * cambio   # USD
roll = contrato.ne(contrato.shift(1))

def backtest(N):
    pyg = (N.shift(1) * dif * mult * cambio).sum(axis=1)
    c = (N.diff().abs() * coste_lado
         + 2 * N.shift(1).abs() * roll * coste_lado).sum(axis=1)
    return (pyg / CAPITAL).loc["2011":], (c / CAPITAL).loc["2011":]

def resumen(r):
    return {"sharpe": r.mean() / r.std() * np.sqrt(252),
            "vol_pct": 100 * r.std() * np.sqrt(252),
            "rent_pct": 100 * r.mean() * 252}

filas = {}
for nombre, b in [("sin buffer", 0.0), ("buffer 10 %", 0.10)]:
    N = contratos(optimo, media, b)
    bruto, coste = backtest(N)
    cambios = (N.diff().abs() > 0).loc["2011":].sum().sum() / 15
    filas[nombre] = {**resumen(bruto - coste),
                     "coste_pct": 100 * coste.sum() / 15,
                     "operaciones_ano": cambios}
pd.DataFrame(filas).T.round(2)
OUT [08]Contratos enteros con margen de no negociación
             sharpe  vol_pct  rent_pct  coste_pct  operaciones_ano
sin buffer     0.92    16.53     15.19       0.91          2134.67
buffer 10 %    0.86    16.34     14.11       0.69          1132.07

El buffer reduce casi a la mitad las operaciones (de 2.135 a 1.132 cambios de posición al año en los 23 mercados) y los costes del 0,91 % al 0,69 % anual. Pero el Sharpe neto baja de 0,92 a 0,86: lo que se ahorra en costes es menos de lo que se pierde por estar más lejos de la posición óptima. No es una regla general. Con 5 millones de USD, costes de medio tick y una volatilidad del 16 %, los costes son poca cosa (0,7-0,9 puntos al año frente a 14-15 de rentabilidad); con más mercados caros, menos capital o un sistema más rápido, el balance cambia, y el primer ejercicio lo comprueba. La diferencia de 0,06 entre las dos filas, además, es pequeña frente al error típico de un Sharpe de 15 años (en torno a 0,25).

Cada regla, el sistema y la respuesta conocida

IN [09]python
def sistema(prevision):
    N = contratos(prevision / 10 * media, media)
    bruto, coste = backtest(N)
    return bruto - coste

netos = {k: sistema(p) for k, p in prev.items()}
netos["tendencia (dos EWMAC)"] = sistema(
    (0.5 * prev["ewmac16_64"] + 0.5 * prev["ewmac64_256"]).clip(
        -20, 20) * min(2.5, 1 / np.sqrt(
        np.array([0.5, 0.5]) @ rho.iloc[:2, :2].to_numpy()
        @ np.array([0.5, 0.5]))))
netos["combinada"] = sistema(combinada)
pd.DataFrame({k: resumen(r) for k, r in netos.items()}).T.round(2)
OUT [09]Cada regla sola frente a la combinación
                       sharpe  vol_pct  rent_pct
ewmac16_64               0.99    19.75     19.47
ewmac64_256              0.60    20.55     12.28
carry                    0.53    14.69      7.82
tendencia (dos EWMAC)    0.86    20.14     17.42
combinada                0.86    16.34     14.11
Fig. — Sharpe neto de cada regla y de la combinación en el sistema de la lección, con su volatilidad realizada (23 futuros sintéticos, 2011-2025)

Netas de costes, de 2011 a 2025: la EWMAC rápida sola da un Sharpe de 0,99, la lenta 0,60, el carry 0,53, las dos EWMAC juntas 0,86 y la combinación de las tres, 0,86 con una volatilidad del 16,3 % (por debajo del 20 % pedido, por la media absoluta de 7,9 de la combinada). La figura de la cabecera dibuja las curvas: 1 USD de 2011 termina en 13,8 con la EWMAC rápida, 4,6 con la lenta, 2,7 con el carry y 6,8 con la combinación.

Que una regla sola haya salido mejor que la combinación no es un argumento para quedarse con ella. Con 15 años, el error típico de un Sharpe ronda 0,25, y elegir a posteriori la mejor de varias reglas correladas es el sesgo de las pruebas múltiples (lección 2.1.8). La combinación tiene menos volatilidad y no depende de que una sola regla siga funcionando.

IN [10]python
neto = netos["combinada"]
tramos = {f"{a}-{b}": float(resumen(neto.loc[str(a):str(b)])["sharpe"])
          for a, b in [(2011, 2015), (2016, 2020), (2021, 2025)]}
capital = (1 + neto).cumprod()
caida = (capital / capital.cummax() - 1).min()
print({k: round(v, 2) for k, v in tramos.items()})
print(f"caída máxima {100 * caida:.1f} %")
comprar = sistema(pd.DataFrame(10.0, index=fechas,
                               columns=MERCADOS))
print(f"comprar y mantener: Sharpe "
      f"{resumen(comprar)['sharpe']:.2f}, correlación con el "
      f"sistema {neto.corr(comprar):.2f}")
OUT [10]Por subperiodos, caída máxima y correlación con comprar
{'2011-2015': 0.96, '2016-2020': 0.61, '2021-2025': 1.02}
caída máxima -30.1 %
comprar y mantener: Sharpe 0.61, correlación con el sistema 0.12

Por quinquenios, el Sharpe de la combinación es 0,96, 0,61 y 1,02; la caída máxima, del 30,1 %. Comprar y mantener con el mismo dimensionamiento (una previsión fija de +10 en todos los mercados) da 0,61, con una correlación de 0,12 con el sistema. El README de los datos mide carteras más sencillas sobre los mismos ficheros (pesos del 10 % entre la volatilidad de 63 sesiones, sin costes, rendimientos simples) y da 0,99 a la tendencia EWMAC 16/64 + 64/256 y 0,53 a comprar y mantener, cerca del 0,61 de esta lección. Esa medida esconde una trampa que conviene ver una vez: con los mismos pesos, basta cambiar el tipo de rendimiento que se promedia para hundir el Sharpe de comprar y mantener:

IN [11]python
log_r = np.log(precio / (precio - dif))          # ln(P_t / P_t−1)
vol63 = log_r.rolling(63, min_periods=40).std() * np.sqrt(252)
pesos = (0.10 / vol63).shift(1)
for nombre, r in [("logarítmicos", log_r),
                  ("aritméticos", np.exp(log_r) - 1)]:
    cartera = (pesos * r).mean(axis=1).iloc[260:]
    print(f"{nombre:12s} Sharpe "
          f"{cartera.mean() / cartera.std() * np.sqrt(252):.2f}")
OUT [11]Rendimientos logarítmicos frente a aritméticos en una cartera
logarítmicos Sharpe 0.23
aritméticos  Sharpe 0.54

Con los mismos pesos, promediar rendimientos logarítmicos da un Sharpe de 0,23 y promediar rendimientos aritméticos, 0,54. El rendimiento de una cartera es la media ponderada de los rendimientos aritméticos; el logarítmico de cada mercado es menor que el aritmético en media varianza (σ2/2\sigma^2/2), unos 6 a 9 puntos al año en una materia prima con un 35-42 % de volatilidad, y esa diferencia se resta a cada mercado volátil de la cartera aunque nadie la pague. Las tablas del README usan rendimientos simples por esa razón. El sistema de esta lección calcula el resultado en dinero, contrato a contrato, y no tiene ese problema.

Lo que el enfoque de Carver resuelve, y lo que no

  • Separa la idea del tamaño. Cada regla solo tiene que producir una previsión en unidades de riesgo; el tamaño lo decide una fórmula común. Añadir una regla no obliga a reescribir el sistema.
  • Hace comparables los mercados. Con la volatilidad en puntos y el riesgo de un contrato, un bono y un índice entran en la misma fórmula.
  • Pone la diversificación en números. FDM e IDM dicen cuánto riesgo de más se puede tomar por regla y por mercado gracias a que no se mueven juntos, con un tope por si las correlaciones mienten.
  • No resuelve la elección de las reglas ni de los pesos (eso es investigación, con todos los sesgos del módulo 2.5), ni el riesgo de cola de las posiciones grandes en bonos (lección 3.1.3), ni la granularidad con poco capital.

Ejercicios

  1. Repite la comparación de buffer con costes de dos ticks por lado en lugar de medio. ¿Sigue bajando el Sharpe con el buffer?
Solución
IN [12]python
coste_lado = (2 * esp.loc[MERCADOS, "valor_tick"]
              + esp.loc[MERCADOS, "comision"]) * cambio
filas = {}
for nombre, b in [("sin buffer", 0.0), ("buffer 10 %", 0.10)]:
    bruto, coste = backtest(contratos(optimo, media, b))
    filas[nombre] = {**resumen(bruto - coste),
                     "coste_pct": 100 * coste.sum() / 15}
coste_lado = (0.5 * esp.loc[MERCADOS, "valor_tick"]
              + esp.loc[MERCADOS, "comision"]) * cambio
pd.DataFrame(filas).T.round(2)
OUT [12]Buffer con costes cuatro veces mayores
             sharpe  vol_pct  rent_pct  coste_pct
sin buffer     0.80    16.53     13.15       2.95
buffer 10 %    0.77    16.34     12.56       2.24

Con dos ticks por lado los costes pasan del 0,91 % al 2,95 % anual sin buffer y del 0,69 % al 2,24 % con él, y el Sharpe neto baja a 0,80 y 0,77. La diferencia a favor de no usar buffer se reduce (de 0,06 a 0,03), pero no cambia de signo: en este sistema, con posiciones medias de varios contratos en casi todos los mercados y previsiones que se mueven despacio, lo que se pierde por alejarse del óptimo pesa más que lo que se ahorra. El buffer no es una mejora automática: su anchura se elige mirando la rotación y el coste por operación de cada mercado, y se comprueba como cualquier otro parámetro. La celda deja coste_lado como estaba para no cambiar las siguientes.

  1. Con un capital de 250.000 USD, ¿cuántos mercados tendrían una posición media de menos de un contrato? ¿Qué le pasa al riesgo realizado del sistema?
Solución
IN [13]python
pequena = idm * w_i * 250_000 * TAU / riesgo_contrato
print("mercados con posición media < 1:",
      int((pequena.loc["2025-11-26"] < 1).sum()), "de", len(MERCADOS))
N = contratos(combinada / 10 * pequena, pequena)
pyg = (N.shift(1) * dif * mult * cambio).sum(axis=1) / 250_000
r = pyg.loc["2011":]
print(f"vol realizada {100 * r.std() * np.sqrt(252):.1f} %, "
      f"días sin posición en RVA "
      f"{100 * (N['RVA'].loc['2011':] == 0).mean():.0f} %")
OUT [13]Sistema con 250.000 USD
mercados con posición media < 1: 19 de 23
vol realizada 13.9 %, días sin posición en RVA 78 %

Con 250.000 USD, 19 de los 23 mercados tienen una posición media de menos de un contrato: su posición entera salta entre 0 y 1 (o 2), y pasan buena parte del tiempo sin posición (en RVA, el 78 % de los días). La volatilidad realizada baja al 13,9 %, lejos del 20 % objetivo, y la diversificación real es mucho menor de la que supone el IDM. Las soluciones son los micros y minis (lección 3.1.1), menos mercados con más peso cada uno, o aceptar el error de seguimiento.

  1. ¿Por qué el escalar de una regla se estima juntando todos los mercados y no uno por mercado? ¿Qué pasaría si cada mercado tuviera el suyo, estimado con toda la muestra?
Solución

Porque el escalar solo debe normalizar la escala de la regla, que es casi la misma en todos los mercados cuando la regla está en unidades de riesgo. Un escalar por mercado con toda la muestra tendría dos problemas. El primero, ruido: en un mercado con pocas tendencias largas, la media absoluta es baja y el escalar alto, y el sistema daría más peso justo donde la regla ha sido más débil. El segundo, anticipación: si el escalar se calcula con datos del futuro, la posición de 2012 depende de cómo fue la regla en 2020. Agrupar mercados y fijar el escalar con una ventana inicial (o con una ventana creciente) evita los dos.

Respuesta plegada en cada pregunta

Autoevaluación

  1. ¿Por qué Carver divide la diferencia de medias de una regla EWMAC entre la volatilidad del precio en puntos?

    • APara que la previsión sea siempre positiva
    • BPara que la previsión mida la fuerza de la tendencia en unidades de riesgo, comparable entre un bono que se mueve 0,46 puntos al día y un índice que se mueve 131
    • CPara que el sistema opere menos
    • DPorque lo exige el bróker
    Ver respuesta

    Respuesta B. Para que la previsión mida la fuerza de la tendencia en unidades de riesgo, comparable entre un bono que se mueve 0,46 puntos al día y un índice que se mueve 131

    Sin tipificar, la diferencia de medias de RVA sería cientos de veces la de TPC solo por la escala del precio. Dividida por la volatilidad diaria en puntos, las dos se miden en desviaciones típicas y se puede usar el mismo escalar en todos los mercados.

  2. El escalar de EWMAC 16/64 estimado con 23 mercados de 2011 a 2013 es 3,74 y la configuración de ejemplo de pysystemtrade usa 3,75. ¿Qué indica?

    • AQue los datos están copiados de pysystemtrade
    • BQue el escalar depende sobre todo de la forma de la regla y poco del mercado: con la previsión en unidades de riesgo, la media absoluta de la regla bruta es parecida en cualquier serie con una volatilidad estimada de forma parecida
    • CQue hay que reestimar el escalar cada día
    • DQue la regla funciona
    Ver respuesta

    Respuesta B. Que el escalar depende sobre todo de la forma de la regla y poco del mercado: con la previsión en unidades de riesgo, la media absoluta de la regla bruta es parecida en cualquier serie con una volatilidad estimada de forma parecida

    El escalar solo normaliza la escala de la regla, y esa escala depende de las ventanas de las medias y de cómo se mide la volatilidad. Por eso se estima agrupando mercados y es estable: con 2014-2025 sale 4,01. No dice nada de si la regla gana dinero.

  3. Tres previsiones con correlaciones de 0,34 a 0,61 y pesos 0,25, 0,25 y 0,50 dan un FDM de 1,263. ¿Para qué sirve?

    • APara apalancar más el sistema sin motivo
    • BPara que la combinación, que tiene menos dispersión que cada previsión por separado al estar poco correladas, vuelva a tener una media absoluta cercana a 10
    • CPara reducir los costes
    • DPara corregir el sesgo de supervivencia
    Ver respuesta

    Respuesta B. Para que la combinación, que tiene menos dispersión que cada previsión por separado al estar poco correladas, vuelva a tener una media absoluta cercana a 10

    Una media ponderada de previsiones poco correladas se mueve menos que cada una. Sin el FDM, el sistema tendría menos riesgo del objetivo. El FDM es 1/√(w′ρw), con un tope (2,5 en pysystemtrade).

  4. En la lección, el buffer del 10 % reduce las operaciones de 2.135 a 1.132 al año y los costes del 0,91 % al 0,69 % anual, pero el Sharpe neto baja de 0,92 a 0,86. ¿Cómo se interpreta?

    • AQue el buffer es un error
    • BQue con estos costes, bajos frente a la volatilidad, lo que se ahorra no compensa lo que se pierde por alejarse de la posición óptima; el buffer gana valor cuando los costes son mayores
    • CQue hay que poner un buffer del 50 %
    • DQue el backtest está mal
    Ver respuesta

    Respuesta B. Que con estos costes, bajos frente a la volatilidad, lo que se ahorra no compensa lo que se pierde por alejarse de la posición óptima; el buffer gana valor cuando los costes son mayores

    El buffer cambia costes por error de seguimiento. Aquí los costes se llevan menos de un punto al año y la diferencia de Sharpe (0,06) es pequeña frente a su error típico. Con costes más altos (más mercados caros, capital más pequeño) el balance cambia.

  5. La combinación (Sharpe 0,86) rinde menos que la regla EWMAC 16/64 sola (0,99). ¿Qué conclusión es razonable?

    • AQuedarse solo con EWMAC 16/64
    • BNinguna fuerte: con 15 años el error típico de un Sharpe ronda 0,25, las reglas están correladas y elegir la mejor a posteriori es sobreajuste; la combinación tiene menos volatilidad y no depende de una sola regla
    • CQue el carry no sirve en ningún mercado
    • DQue hay que cambiar los escalares
    Ver respuesta

    Respuesta B. Ninguna fuerte: con 15 años el error típico de un Sharpe ronda 0,25, las reglas están correladas y elegir la mejor a posteriori es sobreajuste; la combinación tiene menos volatilidad y no depende de una sola regla

    Elegir la regla que mejor salió en el backtest es el error de las pruebas múltiples (lección 2.1.8). Carver combina reglas porque no se sabe de antemano cuál irá mejor; la combinación reduce la dependencia de una sola.

Ideas clave

  • Una previsión es un número continuo en unidades de riesgo: EWMAC = (media rápida − media lenta) / volatilidad del precio en puntos; carry = carry anual / volatilidad anual. Se escala para que su media absoluta sea 10 y se recorta a ±20.
  • Los escalares se estiman agrupando mercados y son estables: 3,74 para EWMAC 16/64 y 1,76 para 64/256 con 2011-2013, cerca de los 3,75 y 1,87 de la configuración de ejemplo de pysystemtrade.
  • La combinación se reescala con el FDM (1,263 con tres previsiones) y el reparto entre mercados con el IDM (2,81 sin tope, 2,5 con él): la diversificación permite más riesgo por mercado para el mismo riesgo total.
  • Contratos = previsión/10 × IDM × peso × capital × riesgo objetivo / riesgo anual de un contrato, en contratos enteros y con un buffer del 10 % de la posición media.
  • Neto de costes, el sistema da un Sharpe de 0,86 con una volatilidad del 16,3 % y una caída máxima del 30,1 % (2011-2025), y 0,96, 0,61 y 1,02 por quinquenios. Los datos son sintéticos con tendencia plantada: el resultado valida el código, no la estrategia.
  • Un rendimiento de cartera se calcula con rendimientos aritméticos: promediar rendimientos logarítmicos ponderados da un Sharpe de 0,23 al mismo comprar y mantener que, bien medido, da 0,54.

Recursos

Datos