---
title: "3.1.4 Sistemas de futuros diversificados (Carver)"
description: "El enfoque de Carver en 23 futuros: previsiones continuas, escalado a 10 y tope de 20, combinación con FDM, IDM, contratos enteros, buffering y costes."
url: "https://pythonparatrading.com/curso-python-trading/nivel-3/sistemas-de-futuros-diversificados-carver/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-3/sistemas-de-futuros-diversificados-carver.png"
---

# 3.1.4 · 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.

- Curso: [Nivel 3: Derivados: futuros, spreads, forex y opciones](https://pythonparatrading.com/curso-python-trading/nivel-3/)
- Módulo: 3.1 · Futuros en profundidad
- Duración: 75 min
- Requisitos: [3.1.3 · Gestión del roll, márgenes, SPAN y apalancamiento implícito](https://pythonparatrading.com/curso-python-trading/nivel-3/roll-margenes-span-y-apalancamiento/), [2.2.2 · Universo, señal, filtro, entrada, salida, sizing y gestión del riesgo](https://pythonparatrading.com/curso-python-trading/nivel-2/universo-senal-filtro-entrada-salida-y-sizing/), [2.3.1 · Seguimiento de tendencia y momentum](https://pythonparatrading.com/curso-python-trading/nivel-2/seguimiento-de-tendencia-y-momentum/), [2.3.4 · Carry en divisas, futuros y bonos; estacionalidad y efectos calendario](https://pythonparatrading.com/curso-python-trading/nivel-2/carry-estacionalidad-y-efectos-calendario/)

## Al terminar esta lección

- Calcular una previsión EWMAC y una de carry como Carver: en unidades de riesgo, escaladas para que su media absoluta sea 10 y recortadas a ±20.
- Estimar los escalares de las previsiones con datos de varios mercados a la vez y compararlos con los de referencia de pysystemtrade.
- Combinar previsiones con pesos y un multiplicador de diversificación (FDM), y repartir el riesgo entre mercados con un IDM.
- Pasar de la previsión al número de contratos con el riesgo objetivo, el nominal y la divisa de cada mercado, y aplicar un buffer de no negociación.
- Medir el sistema neto de costes frente a cada regla sola y frente a la respuesta conocida de los datos, sin confundir lo que se valida (el código) con lo que no (la estrategia en mercados reales).

## 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:

```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:

```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)
```
```output
(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:

$$
\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:

```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})")
```
```output
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.

```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}")
```
```output
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:

```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]]
```
```output
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:

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

con $w$ 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):

```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} %")
```
```output
             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.* Dos paneles con el mismo eje de tiempo, de 2019 a 2023. Arriba, el precio ajustado por diferencia del crudo sintético: en torno a 120 en 2019, cae a unos 35 en 2020, sube hasta cerca de 100 a mediados de 2022 y baja hasta casi 0 a finales de 2023. Abajo, la previsión combinada (destacada) entre los topes de −20 y +20: oscila entre −15 y +10 en 2019, pasa casi todo 2020 en −20, sube hasta +10 en 2021-2022 sin llegar al tope y vuelve a −20 en 2023.

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 $K$, un riesgo objetivo anual $\tau$, el peso del
mercado en la cartera $w_i$ y el riesgo anual en dinero de un contrato (lección 3.1.1):

$$
N_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 $M_i$ es el multiplicador y $X_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 $\tau / n$ en cada uno tiene menos riesgo que $\tau$, y el IDM lo compensa.

```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}")
```
```output
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.

```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)
```
```output
     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`):

```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:

```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)
```
```output
             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

```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)
```
```output
                       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).* Barras horizontales del Sharpe neto: EWMAC 16/64, 0,99 con una volatilidad del 19,8 %; EWMAC 64/256, 0,60 y 20,6 %; carry, 0,53 y 14,7 %; tendencia con las dos EWMAC, 0,86 y 20,1 %; combinada (destacada), 0,86 y 16,3 %; comprar y mantener con el mismo sizing, 0,61.

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.

```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}")
```
```output
{'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:

```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}")
```
```output
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 ($\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.

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

Los datos son sintéticos y la tendencia está plantada: el guion de episodios de las materias primas da derivas que la
curva no descuenta, y la prima de carry existe donde $\kappa_Q > \kappa_P$. Que el sistema gane dinero en estos datos
valida que el código recupera lo plantado, no que la estrategia funcione en mercados reales. Para la evidencia real
sobre tendencia y carry en futuros, la literatura de las lecciones 2.3.1 y 2.3.4.

</aside>

## 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.**

```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)
```
```output
             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.

2. 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.**

```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} %")
```
```output
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.

3. ¿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.

## Autoevaluación

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

   - A. Para que la previsión sea siempre positiva
   - 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
   - C. Para que el sistema opere menos
   - D. Porque lo exige el bróker

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

   - A. Que los datos están copiados de pysystemtrade
   - 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
   - C. Que hay que reestimar el escalar cada día
   - D. Que la regla funciona

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

   - A. Para apalancar más el sistema sin motivo
   - 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
   - C. Para reducir los costes
   - D. Para corregir el sesgo de supervivencia

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

   - A. Que el buffer es un error
   - 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
   - C. Que hay que poner un buffer del 50 %
   - D. Que el backtest está mal

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

   - A. Quedarse solo con EWMAC 16/64
   - 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
   - C. Que el carry no sirve en ningún mercado
   - D. Que hay que cambiar los escalares

   *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

### Research

- [Volatility targeting](https://pythonparatrading.com/volatility-targeting/) · Research-0004 · 8 min

### Documentación

- [pysystemtrade: valores por defecto (tope de 20, media absoluta de 10, multiplicadores con tope de 2,5, buffer del 10 %)](https://github.com/robcarver17/pysystemtrade/blob/a62269d27707806ba61f516791b71a9cd14cf268/sysdata/config/defaults.yaml) · github.com
- [pysystemtrade: configuración de ejemplo del capítulo 15 (escalares de EWMAC y carry, FDM, IDM)](https://github.com/robcarver17/pysystemtrade/blob/a62269d27707806ba61f516791b71a9cd14cf268/systems/provided/futures_chapter15/futuresconfig.yaml) · github.com
- [pysystemtrade: la regla EWMAC (ewmac.py)](https://github.com/robcarver17/pysystemtrade/blob/a62269d27707806ba61f516791b71a9cd14cf268/systems/provided/rules/ewmac.py) · github.com

### Libros

- [Robert Carver: Systematic Trading (Harriman House, 2015)](https://www.harriman-house.com/authors/robert-carver/systematic-trading/9780857194459) · harriman-house.com
- [Robert Carver: Advanced Futures Trading Strategies (Harriman House, 2023)](https://www.harriman-house.com/authors/robert-carver/advanced-futures-trading-strategies/9780857199683) · harriman-house.com

### Datos

- [Datos del nivel 3: curvas de futuros (Parquet)](https://pythonparatrading.com/curso/datos/nivel-3/futuros3_curvas.parquet) · Descarga · futuros3_curvas.parquet

Lección anterior: [3.1.3 · Gestión del roll, márgenes, SPAN y apalancamiento implícito](https://pythonparatrading.com/curso-python-trading/nivel-3/roll-margenes-span-y-apalancamiento/)

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