---
title: "2.5.6 Sensibilidad de parámetros y robustez"
description: "Mesetas frente a picos: rejillas de parámetros en futuros con efecto plantado y en ruido, centro de la meseta y robustez entre activos y periodos."
url: "https://pythonparatrading.com/curso-python-trading/nivel-2/sensibilidad-de-parametros-y-robustez/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-2/sensibilidad-de-parametros-y-robustez.png"
---

# 2.5.6 · Sensibilidad de parámetros: mesetas frente a picos, robustez entre activos y periodos

> Toda estrategia tiene parámetros, y casi cualquier rejilla de parámetros tiene un máximo. Lo que distingue un efecto de un accidente es lo que rodea a ese máximo: una meseta ancha que se repite en otros mercados y en otras épocas, o un pico aislado que se desvanece en cuanto se mira fuera de la muestra.

- Curso: [Nivel 2: Estrategias sistemáticas y backtesting riguroso](https://pythonparatrading.com/curso-python-trading/nivel-2/)
- Módulo: 2.5 · Sesgos y validación
- Duración: 60 min
- Requisitos: [2.3.1 · Seguimiento de tendencia y momentum](https://pythonparatrading.com/curso-python-trading/nivel-2/seguimiento-de-tendencia-y-momentum/), [2.5.2 · In-sample y out-of-sample: walk-forward anclado y móvil](https://pythonparatrading.com/curso-python-trading/nivel-2/in-sample-out-of-sample-y-walk-forward/), [2.5.5 · Monte Carlo: remuestreo de trades, bootstrap de bloques y permutaciones de señales](https://pythonparatrading.com/curso-python-trading/nivel-2/monte-carlo-bootstrap-y-permutaciones/)

## Al terminar esta lección

- Construir una rejilla de parámetros de una estrategia y leer su superficie de Sharpe como un mapa, no como un ranking.
- Medir la suavidad de una superficie con la razón vecinos/pico, la fracción positiva, la rugosidad y la correlación entre combinaciones.
- Aplicar la regla «elegir el centro de una meseta» y comprobar, dentro y fuera de muestra, qué gana frente a elegir el pico.
- Comprobar la robustez de los parámetros elegidos entre mercados, sectores, otra clase de activo y periodos, y saber qué significa que falle en cada uno.

## Un óptimo no es un resultado

Cualquier estrategia tiene parámetros: la longitud de una media, un umbral, una ventana de volatilidad, un plazo de
revisión. Probar varias combinaciones y quedarse con la mejor es inevitable y no es, en sí, un error. El error es leer
la rejilla como un ranking y creer que el número de la primera fila es lo que la estrategia dará en el futuro. La
lección 2.5.4 corrige ese número por el número de pruebas; esta mira la forma de la rejilla, que dice algo que el
número solo no dice.

La intuición es mecánica. Si hay un efecto real que la estrategia captura, cambiar un poco un parámetro cambia poco lo
que captura: una media de 125 sesiones y una de 150 se parecen mucho. El resultado forma una **meseta**. Si no hay
efecto, el máximo de la rejilla es el ganador de una lotería: una combinación que acertó por casualidad los días
buenos de esta muestra, y sus vecinas, que acertaron otros días o ninguno, no se le parecen. El resultado es un
**pico**.

```mermaid
flowchart TB
  G[Rejilla] --> S[Superficie de Sharpe]
  S --> M[Métricas de suavidad]
  M --> C[Centro de meseta]
  C --> F[Fuera de muestra]
  F --> A[Otros activos]
  A --> P[Otros periodos]
  P --> D[Candidata]
```

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

Los datos son sintéticos. Los futuros del nivel 2 llevan un momentum de series temporales **plantado** (README del nivel
2: `0,15·σ·signo(rendimiento de las 252 sesiones anteriores)`, más una prima de carry y la de los índices), y la serie
`ruptura` de `regimenes2.parquet` es, desde el 2013-05-22, ruido i.i.d. sin ningún efecto. Las cifras de esta lección
sirven para comprobar si cada método distingue los dos casos, no como evidencia de lo que darían estas reglas en
mercados reales.

</aside>

## La rejilla en los futuros: una meseta

La estrategia es un cruce de medias sobre los 24 futuros: en cada mercado, largo si la media de las últimas $f$
sesiones está por encima de la de las últimas $s$ y corto si está por debajo, con la posición escalada por la
inversa de la volatilidad y ejecutada en la sesión siguiente (lección 2.3.1). Ocho medias rápidas por ocho lentas
dan 64 combinaciones:

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

def sharpe(x):
    x = np.asarray(x, dtype=float)
    x = x[~np.isnan(x)]
    return np.mean(x) / np.std(x, ddof=1) * np.sqrt(252)

fut = pd.read_parquet("futuros2_continuos.parquet")
px = fut.pivot(index="fecha", columns="mercado",
               values="cierre_ajustado")
r = px.pct_change()
vol = r.ewm(span=60, min_periods=60).std() * np.sqrt(252)
escala = 0.30 / vol / px.shape[1]

RAPIDAS = [5, 10, 20, 30, 40, 50, 60, 80]
LENTAS = [100, 125, 150, 175, 200, 250, 300, 350]
medias = {n: px.rolling(n).mean() for n in RAPIDAS + LENTAS}

def pnl_cruce(f, s):
    """Por mercado: largo si la media rápida está sobre la lenta."""
    signo = np.sign(medias[f] - medias[s])
    return ((signo * escala).shift(1) * r).loc["2001-01-02":]

por_mercado = {(f, s): pnl_cruce(f, s) for f in RAPIDAS for s in LENTAS}
carteras = pd.DataFrame({k: v.sum(axis=1)
                         for k, v in por_mercado.items()})

def rejilla(carteras, desde=None, hasta=None):
    sr = carteras.loc[desde:hasta].apply(sharpe)
    return sr.unstack().rename_axis(index="rápida", columns="lenta")

g_fut = rejilla(carteras)
g_fut.round(2)
```
```output
lenta    100   125   150   175   200   250   300   350
rápida
5       0.71  0.68  0.73  0.81  0.82  0.90  1.04  1.09
10      0.60  0.72  0.83  0.82  0.83  0.92  1.07  1.08
20      0.67  0.78  0.80  0.87  0.78  0.97  1.06  1.12
30      0.74  0.89  0.86  0.89  0.91  0.97  1.10  1.12
40      0.83  0.87  0.88  0.93  0.91  0.97  1.08  1.03
50      0.86  0.77  0.83  0.90  0.88  0.93  1.04  1.03
60      0.87  0.75  0.89  0.78  0.83  0.86  0.97  0.98
80      0.70  0.72  0.69  0.75  0.83  0.76  1.00  0.97
```

Las 64 combinaciones ganan, con Sharpe entre 0,60 y 1,12. Hay estructura: la superficie sube hacia las medias lentas
largas (las columnas de 300 y 350 sesiones van de 0,97 a 1,12) y baja algo con la rápida de 80 y con las lentas
cortas. Pero ninguna combinación está sola: es la meseta del panel izquierdo de la figura de la cabecera, donde todos
los cuadros son azules y del tamaño máximo. El efecto plantado es el signo de 12 meses (252 sesiones); ninguna de
estas reglas lo calcula exactamente, todas lo capturan en parte porque las tendencias largas mueven las medias de
cualquier longitud, y las que más se le acercan son las de media lenta larga: una media de $n$ sesiones va retrasada
$(n - 1)/2$, así que la de 300 mira, en promedio, unos 150 días atrás.

## La misma búsqueda en ruido: picos

Para ver un pico hace falta un caso sin efecto. La serie de control del README, `ruptura` después de su ruptura, es
ruido normal i.i.d. de media cero. Sobre ella se prueba una familia de reglas de reversión de corto plazo, que es donde
suelen acabar las búsquedas de un investigador impaciente: ir contra el signo de la rentabilidad de $n$ sesiones y
revisar la posición cada $m$ sesiones. Nueve por seis, 54 combinaciones, todas siempre en el mercado:

```python
reg = pd.read_parquet("regimenes2.parquet")
ru = (reg[reg["serie"] == "ruptura"].set_index("fecha")["precio"]
      .loc["2013-05-22":])          # tras la ruptura: i.i.d., media 0
r_ru = ru.pct_change()

DIAS = [1, 2, 3, 4, 5, 7, 10, 15, 20]
CADA = [1, 2, 3, 5, 7, 10]

def pnl_reversion(n, m):
    """Contra el signo de la rentabilidad de n sesiones; la
    posición se revisa cada m sesiones."""
    signo = -np.sign(ru.pct_change(n))
    signo = signo.where(np.arange(len(signo)) % m == 0).ffill()
    return (signo.shift(1) * r_ru).loc["2014-01-02":]

carteras_ru = pd.DataFrame({(n, m): pnl_reversion(n, m)
                            for n in DIAS for m in CADA})
g_ru = rejilla(carteras_ru).rename_axis(index="días",
                                        columns="cada")
g_ru.round(2)
```
```output
cada    1     2     3     5     7     10
días
1    -0.02  0.00 -0.02  0.14 -0.46 -0.05
2    -0.29 -0.26 -0.42  0.01 -0.07 -0.02
3    -0.24 -0.38 -0.26 -0.13 -0.27  0.28
4    -0.11 -0.10  0.12  0.11 -0.46  0.57
5     0.02  0.07  0.05 -0.23  0.13  0.38
7    -0.01 -0.03 -0.21 -0.28  0.00  0.16
10   -0.22 -0.36 -0.13 -0.02 -0.01  0.24
15   -0.01 -0.00  0.27  0.34  0.08  0.19
20    0.25  0.09 -0.21 -0.05 -0.11  0.23
```

La mejor combinación, ir contra la rentabilidad de 4 sesiones revisando cada 10, da un Sharpe de 0,57 en doce años:
presentable en un informe. Está en el borde de la rejilla, y sus cinco vecinas valen 0,28, −0,27, −0,46, 0,13 y 0,38:
de media, 0,01. Es el panel derecho de la figura de la cabecera: cuadros azules y naranjas de todos los
tamaños mezclados sin orden. No hay nada que capturar, y cada combinación ha capturado un azar distinto.

## Medir la suavidad

Mirar la superficie es lo primero, pero conviene resumirla en cifras que se puedan comparar entre estrategias y
guardar en el diario de experimentos (lección 2.2.4). Cuatro sencillas:

- **Vecinos/pico**: media de las celdas que rodean al máximo (8, o menos en un borde) dividida por el máximo. Cerca de 1, meseta; cerca de
  0 o negativa, pico aislado.
- **Fracción positiva**: qué parte de la rejilla gana. Sin efecto, en torno a la mitad.
- **Rugosidad**: salto medio entre celdas contiguas dividido por la desviación típica de la rejilla. En una
  superficie de ruido blanco (celdas independientes con la misma dispersión), el salto medio entre dos celdas es
  $E|X - Y| = 2\sigma/\sqrt{\pi}$, así que la rugosidad vale $2/\sqrt{\pi} \approx 1{,}13$. Una meseta da bastante
  menos.
- **Correlación entre combinaciones**: cuánto se parecen las series de rendimientos de la rejilla entre sí.

```python
def vecinos(a, i, j):
    filas, cols = a.shape
    return [a[x, y] for x in range(i - 1, i + 2)
            for y in range(j - 1, j + 2)
            if (x, y) != (i, j) and 0 <= x < filas and 0 <= y < cols]

def suavidad(g):
    a = g.to_numpy()
    i, j = np.unravel_index(np.argmax(a), a.shape)
    v = np.mean(vecinos(a, i, j))
    saltos = np.r_[np.abs(np.diff(a, axis=0)).ravel(),
                   np.abs(np.diff(a, axis=1)).ravel()]
    m = {"pico": a[i, j], "media_vecinos": v,
         "vecinos/pico": v / a[i, j], "positivas": (a > 0).mean(),
         "rugosidad": saltos.mean() / a.std()}
    return {"en": f"{g.index[i]}/{g.columns[j]}",
            **{k: float(round(x, 2)) for k, x in m.items()}}

pd.DataFrame({"futuros": suavidad(g_fut), "ruido": suavidad(g_ru)})
```
```output
              futuros ruido
en             20/350  4/10
pico             1.12  0.57
media_vecinos    1.09  0.01
vecinos/pico     0.97  0.02
positivas         1.0  0.41
rugosidad        0.47  0.94
```

Las cifras dicen lo mismo que la figura. En los futuros, las vecinas del pico (cinco, porque está en el borde) conservan
el 97 % de su Sharpe, todas las celdas son positivas y la rugosidad es 0,47. En el ruido, las vecinas conservan un 2 %,
solo el 41 % de las celdas gana y la rugosidad, 0,94, está cerca del 1,13 del ruido blanco.

La cuarta métrica explica parte de la diferencia, y es una advertencia:

```python
def parecido(carteras):
    c = carteras.dropna().corr().to_numpy()
    n = len(c)
    autovalores = np.linalg.eigvalsh(c)
    return {"combinaciones": n,
            "correlación_media": (c.sum() - n) / (n * (n - 1)),
            "primer_componente": autovalores[-1] / n}

pd.DataFrame({"futuros": parecido(carteras),
              "ruido": parecido(carteras_ru)}).round(2)
```
```output
                   futuros  ruido
combinaciones        64.00  54.00
correlación_media     0.77   0.32
primer_componente     0.78   0.35
```

Las 64 variantes del cruce de medias tienen una correlación media de 0,77, y un solo componente principal explica el
78 % de su varianza: son casi la misma estrategia con pequeñas diferencias. Las de reversión se parecen mucho menos
(0,32). Una parte de la suavidad de los futuros viene de ahí, y no del efecto: con parámetros tan correlados, una
superficie puede salir suave también sobre ruido, sobre todo con medias largas (el ejercicio 2 lo comprueba). La
suavidad es **necesaria** para fiarse de unos parámetros, pero no **suficiente**. Esta correlación es, además, lo que
hay que tener en cuenta al contar el número de pruebas para el Deflated Sharpe (lección 2.5.4): 64 variantes casi
idénticas no son 64 intentos independientes.

## Elegir el centro de la meseta

La regla práctica que se deduce: **no elijas el máximo de la rejilla; elige el centro de la meseta**. Una forma
explícita es suavizar la superficie (cada celda pasa a valer la media de su vecindad de 3 × 3) y quedarse con el
máximo de la suavizada. La celda elegida es la que mejor está rodeada, y su valor suavizado es una estimación más
prudente de lo que cabe esperar.

$$
\overline{S}_{ij} = \frac{1}{9}\sum_{a=-1}^{1}\sum_{b=-1}^{1} S_{i+a,\,j+b}
$$

Los bordes de la rejilla no tienen vecindad completa, y una media con menos vecinas es más ruidosa: se dejan fuera.
Si el máximo suavizado quedara pegado al borde, la conclusión sería ampliar la rejilla, no elegir el borde.

```python
def suavizar(g):
    """Media de cada celda con sus 8 vecinas. Los bordes quedan
    vacíos: no tienen vecindad completa."""
    a = g.to_numpy()
    f, c = a.shape
    s = np.full((f, c), np.nan)
    for i in range(1, f - 1):
        for j in range(1, c - 1):
            s[i, j] = a[i - 1:i + 2, j - 1:j + 2].mean()
    return pd.DataFrame(s, index=g.index, columns=g.columns)

def elegir(g):
    """Pico bruto y centro de la meseta (máximo de la suavizada)."""
    return g.stack().idxmax(), suavizar(g).stack().idxmax()

def txt(k):
    return f"{k[0]}/{k[1]}"

for nombre, g in (("futuros", g_fut), ("ruido", g_ru)):
    pico, centro = elegir(g)
    print(f"{nombre}: pico {txt(pico)} ({g.loc[pico]:.2f}); "
          f"centro {txt(centro)} ({g.loc[centro]:.2f}, "
          f"vecindad {suavizar(g).loc[centro]:.2f})")
```
```output
futuros: pico 20/350 (1.12); centro 30/300 (1.10, vecindad 1.05)
ruido: pico 4/10 (0.57); centro 15/7 (0.08, vecindad 0.10)
```

En los futuros, el pico bruto (20/350) está en el borde, y el centro de la meseta es el cruce 30/300: Sharpe 1,10, con
una vecindad que promedia 1,05. Está en la última columna interior: la meseta sigue subiendo hacia las medias lentas y
la rejilla se queda corta por ese lado. Es el caso en que la regla pide ampliar la rejilla; el ejercicio 1 lo hace, y el
centro apenas se mueve. En el ruido, el centro es 15/7, con 0,08 y una vecindad de 0,10: la regla no encuentra ninguna
meseta, y eso ya es un resultado. Si el investigador del ruido hubiera usado esta regla, habría visto que su «mejor
estrategia» de 0,57 está rodeada de nada.

La prueba de la regla es fuera de muestra. Se elige con la primera mitad de los datos y se mide en la segunda: en los
futuros, 2001-2012 frente a 2013-2025; en el ruido, 2014-2019 frente a 2020-2025 (lección 2.5.2).

```python
casos = {
    "futuros": (carteras, ("2001", "2012"), ("2013", "2025")),
    "ruido": (carteras_ru, ("2014", "2019"), ("2020", "2025")),
}
filas, dentro_fuera = {}, {}
for nombre, (c, ins, oos) in casos.items():
    g_in, g_out = rejilla(c, *ins), rejilla(c, *oos)
    dentro_fuera[nombre] = (g_in, g_out)
    pico, centro = elegir(g_in)
    for regla, k in (("pico", pico), ("centro", centro)):
        filas[(nombre, regla, txt(k))] = {"dentro": g_in.loc[k],
                                          "fuera": g_out.loc[k]}
    filas[(nombre, "mediana", "todas")] = {
        "dentro": g_in.stack().median(),
        "fuera": g_out.stack().median()}
    rho = np.corrcoef(g_in.to_numpy().ravel(),
                      g_out.to_numpy().ravel())[0, 1]
    print(f"{nombre}: correlación dentro-fuera de la rejilla {rho:.2f}")
pd.DataFrame(filas).T.round(2)
```
```output
futuros: correlación dentro-fuera de la rejilla 0.27
ruido: correlación dentro-fuera de la rejilla 0.15
                        dentro  fuera
futuros pico    20/175    0.96   0.79
        centro  20/300    0.89   1.23
        mediana todas     0.77   0.99
ruido   pico    5/10      0.63   0.13
        centro  15/7      0.21  -0.05
        mediana todas    -0.04  -0.05
```

*Fig. — Sharpe dentro y fuera de muestra de cada combinación: futuros (2001-2012 frente a 2013-2025) y ruido (2014-2019 frente a 2020-2025).* Dispersión con 64 puntos azules de los futuros, arriba a la derecha y casi todos por encima de la diagonal (dentro, de 0,39 a 0,96; fuera, de 0,66 a 1,37), y 54 puntos grises del ruido, alrededor del origen. La diagonal marca dentro igual a fuera. El pico del ruido, destacado, pasa de 0,63 dentro a 0,13 fuera; el de los futuros, de 0,96 a 0,79, y el centro de los futuros, de 0,89 a 1,23.

En los futuros, todo aguanta: el pico elegido en 2001-2012 (20/175, Sharpe 0,96) da 0,79 en 2013-2025; el centro
(20/300, 0,89), 1,23; la mediana de las 64 combinaciones pasa de 0,77 a 0,99. La segunda mitad es más favorable a la
tendencia que la primera, y sobre todo a las medias lentas largas, que es donde cae el centro. No hay que leer en esto
que el centro gane siempre: en una meseta real, elegir el centro en lugar del pico cuesta poco o nada fuera de muestra,
y su valor de dentro no es el máximo de un sorteo.

En el ruido, el pico de 2014-2019 (5/10, 0,63) da 0,13 en 2020-2025, y la mediana de la rejilla es −0,04 dentro y −0,05
fuera: la rejilla entera vale cero y el pico era el premio de la lotería. La correlación entre la rejilla de dentro y
la de fuera, en cambio, separa poco los dos casos: 0,27 en los futuros y 0,15 en el ruido. En los futuros cambia la
forma (dentro de muestra la superficie es plana, con el pico en 175; fuera mandan las lentas largas) aunque el nivel
se mantenga. Lo que separa los dos casos es el nivel de toda la rejilla fuera de muestra (mediana de 0,99 frente a
−0,05), no el orden de sus celdas.

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

Fíjate en que el pico del ruido no cae a cero exacto fuera de muestra: 0,13. Con seis años fuera de muestra, el Sharpe
de una regla sin ventaja tiene un error típico de unos 0,4 ($1/\sqrt{6}$), así que puede salir cualquier cosa entre
−0,8 y 0,8.
Una sola comprobación fuera de muestra no separa un efecto pequeño de la suerte: por eso se combina con la forma de la
superficie, con la robustez entre activos y con el Deflated Sharpe.

</aside>

```mermaid
flowchart TB
  S{¿Meseta ancha?} -- no --> X[Descartar]
  S -- sí --> O{¿Aguanta fuera?}
  O -- no --> X
  O -- sí --> A{¿Otros mercados?}
  A -- no --> R[Revisar hipótesis]
  A -- sí --> P{¿Otros periodos?}
  P -- no --> R
  P -- sí --> C[Candidata]
```

## Robustez entre activos

Un efecto real debería aparecer en todos los mercados donde la hipótesis dice que está, no solo en la cartera agregada.
Una cartera de 24 mercados puede tener un buen Sharpe porque dos o tres mercados extraordinarios compensan a veinte
mediocres, y entonces el resultado depende de que esos dos o tres sigan igual. Con los parámetros del centro de la
meseta:

```python
esp = pd.read_csv("futuros2_especificaciones.csv",
                  index_col="mercado")
CENTRO = elegir(g_fut)[1]
pm = por_mercado[CENTRO]
por_merc = pm.apply(sharpe)
tabla = pd.DataFrame({
    "mercados": por_merc.groupby(esp["sector"]).size(),
    "positivos": (por_merc > 0).groupby(esp["sector"]).sum(),
    "sharpe_medio": por_merc.groupby(esp["sector"]).mean(),
    "sharpe_sector": pm.T.groupby(esp["sector"]).sum().T.apply(sharpe),
})
print(f"Cruce {txt(CENTRO)}: {(por_merc > 0).sum()} de 24 mercados "
      f"con Sharpe positivo")
tabla.round(2)
```
```output
Cruce 30/300: 21 de 24 mercados con Sharpe positivo
                mercados  positivos  sharpe_medio  sharpe_sector
sector
agricolas              4          4          0.46           0.85
bonos                  5          5          0.38           0.53
divisas                3          2          0.15           0.23
energia                3          2          0.15           0.22
metales                4          3          0.17           0.29
renta_variable         5          5          0.46           0.59
```

Veintiuno de los 24 mercados son positivos (los tres negativos están en las divisas, la energía y los metales), y todos
los sectores, como cartera, también: de 0,22 en la energía a 0,85 en los agrícolas. El Sharpe medio por mercado (de 0,15
a 0,46) es mucho menor que el de la cartera (1,10): la diversificación entre mercados poco correlados hace la mayor
parte del trabajo. Que un mercado suelto dé un Sharpe de 0,1 o 0,2 no es una señal de alarma; que la mayoría diera
negativo, sí.

La comprobación más dura es una clase de activo distinta. El nivel 2 tiene diez divisas al contado en
`fx2_carry.parquet`, y su README no les planta momentum: su respuesta conocida es un carry (el contado se deprecia
solo un 30 % del diferencial de tipos, así que el carry gana un 70 %) y unas crisis de carry. La misma regla, sin tocar un parámetro:

```python
fx = pd.read_parquet("fx2_carry.parquet")
spot = fx.pivot(index="fecha", columns="divisa", values="spot")
r_fx = spot.pct_change()
vol_fx = r_fx.ewm(span=60, min_periods=60).std() * np.sqrt(252)
f, s = CENTRO
signo_fx = np.sign(spot.rolling(f).mean() - spot.rolling(s).mean())
pnl_fx = ((signo_fx * 0.30 / vol_fx / 10).shift(1) * r_fx)
pnl_fx = pnl_fx.loc["2006-01-02":]
print(f"Divisas, cruce {txt(CENTRO)}: "
      f"Sharpe {sharpe(pnl_fx.sum(axis=1)):.2f}")
print(pnl_fx.apply(sharpe).round(2).sort_values().to_dict())
```
```output
Divisas, cruce 30/300: Sharpe -0.03
{'EST': -0.6, 'ISL': -0.16, 'JAR': -0.14, 'HUR': -0.11, 'GAL': -0.06, 'BOR': 0.03, 'ALV': 0.15, 'DUN': 0.23, 'FOR': 0.24, 'CAS': 0.29}
```

*Fig. — Robustez entre activos del cruce 30/300: Sharpe de cada mercado (marcas) y de cada sector como cartera (puntos), con las divisas al contado de fx2, que no llevan el efecto.* Gráfico de franjas por sector. En los seis sectores de futuros casi todas las marcas están a la derecha del cero (21 de 24 mercados positivos) y las carteras de sector van de 0,22 en energía a 0,85 en agrícolas. En las diez divisas al contado, sin efecto plantado, las marcas se reparten a ambos lados del cero, de −0,60 a 0,29, y su cartera da −0,03. La línea destacada es la cartera completa de futuros, 1,10.

En las divisas al contado la regla da −0,03: cinco divisas con Sharpe negativo y cinco con positivo, de −0,60 a 0,29.
La deriva del contado por el diferencial de tipos es persistente y una regla de tendencia podría seguirla a ratos, pero
aquí no recoge nada: lejos del 1,10 de los futuros. Esto **no** invalida la estrategia: confirma que el efecto está
donde la hipótesis dice y no en otro sitio. La robustez entre activos se exige dentro del dominio de la hipótesis
(lección 2.2.1). En mercados reales, la literatura documenta el momentum de series temporales en futuros de índices,
bonos, materias primas y divisas[^1]; un efecto que solo apareciera en uno de esos grupos, o solo en la cartera
agregada, sería una señal para revisar la hipótesis antes de operar.

## Robustez entre periodos

El último eje es el tiempo: unos parámetros buenos en 25 años pueden serlo solo porque una década fue extraordinaria.
Se parte la muestra en tramos y se mira el centro de la meseta y la peor combinación de la rejilla en cada uno:

```python
PICO_RU = elegir(dentro_fuera["ruido"][0])[0]
tramos = {"2001-05": ("2001", "2005"), "2006-10": ("2006", "2010"),
          "2011-15": ("2011", "2015"), "2016-20": ("2016", "2020"),
          "2021-25": ("2021", "2025")}
periodos = pd.DataFrame({
    "futuros_centro": {k: sharpe(carteras[CENTRO].loc[a:b])
                       for k, (a, b) in tramos.items()},
    "futuros_peor": {k: carteras.loc[a:b].apply(sharpe).min()
                     for k, (a, b) in tramos.items()},
})
bloques_ru = {"2014-16": ("2014", "2016"), "2017-19": ("2017", "2019"),
              "2020-22": ("2020", "2022"), "2023-25": ("2023", "2025")}
print(periodos.round(2))
print(f"Ruido, pico de 2014-2019 ({txt(PICO_RU)}):",
      {k: float(round(sharpe(carteras_ru[PICO_RU].loc[a:b]), 2))
       for k, (a, b) in bloques_ru.items()})
```
```output
         futuros_centro  futuros_peor
2001-05            1.27          0.56
2006-10            0.62         -0.41
2011-15            1.25          0.28
2016-20            0.88          0.14
2021-25            1.47          0.72
Ruido, pico de 2014-2019 (5/10): {'2014-16': 0.33, '2017-19': 0.93, '2020-22': 0.13, '2023-25': 0.13}
```

El cruce 30/300 es positivo en los cinco quinquenios, de 0,62 en 2006-2010 a 1,47 en 2021-2025. Y la peor de las 64
combinaciones solo es negativa en uno (−0,41 en 2006-2010): la meseta no es un accidente de una época. El quinquenio
más fuerte contiene la caída de los bonos que el guion del generador planta en 2022, y el primero (1,27), la caída de la
renta variable de 2000-2002: tramos de tendencia larga, que es exactamente lo que debe capturar una regla de
tendencia.

El pico del ruido cuenta la historia contraria: 0,33 y 0,93 en los dos trienios con los que se eligió, y 0,13 y 0,13
en los dos siguientes. La mejora dentro de muestra era el criterio de selección; fuera, solo queda ruido.

Resumen para el diario de experimentos de una estrategia candidata:

| Prueba | Cruce 30/300 en futuros | Reversión 5/10 en ruido |
|---|---|---|
| Vecinos/pico | 0,97 | 0,02 |
| Celdas positivas | 100 % | 41 % |
| Rugosidad (ruido blanco: 1,13) | 0,47 | 0,94 |
| Dentro / fuera de muestra (elegido dentro) | 0,89 / 1,23 (20/300) | 0,63 / 0,13 |
| Mercados con Sharpe positivo | 21 de 24 | No aplicable (una serie) |
| Periodos positivos | 5 de 5 | 4 de 4, cayendo de 0,93 a 0,13 |

## Ejercicios

1. Amplía la rejilla de los futuros con medias lentas de 400 y 500 sesiones y una rápida de 90. ¿Se mueve el
   centro de la meseta? ¿Cambian las métricas de suavidad?

**Solución.**

```python
RAPIDAS2 = RAPIDAS + [90]
LENTAS2 = LENTAS + [400, 500]
for n in RAPIDAS2 + LENTAS2:
    if n not in medias:
        medias[n] = px.rolling(n).mean()
carteras2 = pd.DataFrame({
    (f, s): pnl_cruce(f, s).sum(axis=1)
    for f in RAPIDAS2 for s in LENTAS2}).loc["2003-01-02":]
g2 = rejilla(carteras2)
pico2, centro2 = elegir(g2)
print(f"pico {txt(pico2)} ({g2.loc[pico2]:.2f}); "
      f"centro {txt(centro2)} ({g2.loc[centro2]:.2f})")
print(suavidad(g2))
```
```output
pico 20/350 (1.16); centro 20/350 (1.16)
{'en': '20/350', 'pico': 1.16, 'media_vecinos': 1.12, 'vecinos/pico': 0.96, 'positivas': 1.0, 'rugosidad': 0.5}
```

Al ampliar la rejilla (y empezar en 2003 para que la media de 500 sesiones exista desde el principio), el máximo
queda en 20/350 con 1,16, ahora rodeado por todos los lados, y pico y centro coinciden: a una celda del 30/300 de la
rejilla original. Las medias de 400 y 500 sesiones no mejoran a la de 350: la meseta culmina ahí, a la altura del año
que usa el efecto plantado. Las métricas apenas cambian: vecinos/pico 0,96, todas las celdas positivas y rugosidad
0,50. Es lo que se espera de una meseta: añadir parámetros alrededor no la rompe ni la traslada lejos. Un pico, en cambio, suele desaparecer o cambiar de sitio al cambiar la
rejilla o el periodo.

2. Calcula la rejilla de cruces de medias en el gemelo sin memoria de los futuros de la lección 2.5.5 (las mismas
   sesiones en otro orden) y sus métricas de suavidad. ¿Basta la suavidad para distinguirlo de los futuros?

**Solución.**

```python
rng = np.random.default_rng(2028)
orden = rng.permutation(len(r) - 1)
r_g = pd.DataFrame(r.to_numpy()[1:][orden], index=r.index[1:],
                   columns=r.columns)
px_g = (1 + r_g).cumprod()
medias_g = {n: px_g.rolling(n).mean() for n in RAPIDAS + LENTAS}
vol_g = r_g.ewm(span=60, min_periods=60).std() * np.sqrt(252)
esc_g = 0.30 / vol_g / 24
carteras_g = pd.DataFrame({
    (f, s): ((np.sign(medias_g[f] - medias_g[s]) * esc_g).shift(1)
             * r_g).loc["2001-01-02":].sum(axis=1)
    for f in RAPIDAS for s in LENTAS})
g_g = rejilla(carteras_g)
print(suavidad(g_g))
print({k: round(float(v), 2) for k, v in parecido(carteras_g).items()})
```
```output
{'en': '80/150', 'pico': 0.12, 'media_vecinos': -0.02, 'vecinos/pico': -0.18, 'positivas': 0.22, 'rugosidad': 0.68}
{'combinaciones': 64.0, 'correlación_media': 0.75, 'primer_componente': 0.76}
```

El gemelo da una superficie **suave** (rugosidad 0,68, lejos del 1,13 del ruido blanco, y correlación media de 0,75
entre combinaciones), pero plana y sin nada: solo el 22 % de las celdas es positivo y el pico, 80/150, vale 0,12 con
vecinas que promedian −0,02. La suavidad sola no distingue un efecto de su ausencia: con medias largas y muy
correladas, cualquier serie da una superficie suave. Lo que distingue a los futuros es que la meseta es suave **y
alta**: todas las celdas positivas y vecinas que conservan el 97 % del pico. Por eso las métricas se leen juntas.

3. Un compañero presenta una estrategia con un Sharpe de 1,4 en el mejor de 300 conjuntos de parámetros. Su rejilla
   tiene una forma suave, pero solo se ha probado en el índice de un país entre 2010 y 2020. ¿Qué tres comprobaciones
   le pedirías antes de dedicarle más tiempo, y qué resultado de cada una te haría descartarla?

**Solución.**

Primero, el centro de la meseta y su valor suavizado, con los bordes fuera, y la comparación dentro y fuera de muestra
eligiendo los parámetros solo con la primera parte: si el centro baja mucho del 1,4 o la rejilla de fuera no se parece
a la de dentro, el 1,4 era el máximo de una lotería. Segundo, la robustez entre activos donde su hipótesis predice el
efecto (otros índices, otros países): si solo funciona en el que se ha optimizado, no hay efecto general. Tercero, la
robustez entre periodos, con tramos anteriores a 2010 y posteriores a 2020: si todo el resultado viene de unos pocos
años, depende de un régimen. Y, en paralelo, el número real de pruebas (300, corregido por su correlación) para el
Deflated Sharpe de la lección 2.5.4. Una rejilla suave no basta: parámetros vecinos están correlados y la suavidad
puede aparecer también sin efecto.

[^1]: Moskowitz, Ooi y Pedersen (2012), «Time series momentum», Journal of Financial Economics 104(2), 228-250,
    [doi:10.1016/j.jfineco.2011.11.003](https://doi.org/10.1016/j.jfineco.2011.11.003): según su resumen, momentum
    significativo en cada uno de los 58 futuros líquidos estudiados, de índices, divisas, materias primas y bonos.

## Autoevaluación

1. En la rejilla de los futuros, el pico (20/350, en el borde) tiene un Sharpe de 1,12 y sus cinco vecinas promedian 1,09. En el ruido, el pico (4/10) tiene 0,57 y sus vecinas, 0,01. ¿Qué dice esa diferencia?

   - A. Que el ruido tiene un pico más estrecho pero igual de bueno
   - B. Que en los futuros el resultado no depende del valor exacto de los parámetros (meseta) y en el ruido sí: un pico rodeado de ceros es casi siempre azar
   - C. Que hay que usar rejillas más finas en el ruido
   - D. Nada: solo importa el Sharpe del pico

   *Respuesta B: Que en los futuros el resultado no depende del valor exacto de los parámetros (meseta) y en el ruido sí: un pico rodeado de ceros es casi siempre azar.* Si el efecto es real, parámetros cercanos capturan casi lo mismo y dan resultados parecidos. Un máximo cuyas vecinas no ganan nada es lo que produce el azar cuando se prueba mucho: la razón vecinos/pico es 0,97 en los futuros y 0,02 en el ruido.

2. Las 64 combinaciones de la rejilla de futuros tienen una correlación media de 0,77 entre sí. ¿Qué implica?

   - A. Que la rejilla está mal construida
   - B. Que son, en la práctica, pocas estrategias distintas: es parte de por qué la superficie es suave, y es lo que hay que tener en cuenta al contar las pruebas para el Deflated Sharpe
   - C. Que hay 64 pruebas independientes
   - D. Que la estrategia no tiene ventaja

   *Respuesta B: Que son, en la práctica, pocas estrategias distintas: es parte de por qué la superficie es suave, y es lo que hay que tener en cuenta al contar las pruebas para el Deflated Sharpe.* Un primer componente que explica el 78 % de la varianza común indica que las 64 variantes se mueven casi a la vez. La suavidad viene en parte de ahí: es necesaria, pero por sí sola no prueba un efecto. Por eso se añaden la comprobación fuera de muestra y la robustez entre mercados y periodos.

3. ¿Por qué la función `suavizar` de la lección deja vacíos los bordes de la rejilla?

   - A. Para que el código sea más corto
   - B. Porque una celda del borde no tiene vecindad completa: su media se haría con menos vecinas y saldría más ruidosa. Si el máximo suavizado cae en el borde, hay que ampliar la rejilla
   - C. Porque los bordes siempre son peores
   - D. Porque así se elige siempre el centro geométrico

   *Respuesta B: Porque una celda del borde no tiene vecindad completa: su media se haría con menos vecinas y saldría más ruidosa. Si el máximo suavizado cae en el borde, hay que ampliar la rejilla.* Con menos vecinas, la media del borde es menos fiable y tiende a ganar por azar. En la rejilla de futuros el pico bruto (20/350) está en el borde y la superficie sube hacia las medias lentas largas: la señal es ampliar la rejilla. Ampliada (ejercicio 1), el máximo queda en 20/350 rodeado por todos los lados.

4. Elegidos con los datos de 2001-2012, el pico de los futuros (20/175) pasa de 0,96 a 0,79 fuera de muestra y el centro de la meseta (20/300), de 0,89 a 1,23. En el ruido, el pico pasa de 0,63 a 0,13. ¿Qué enseña la regla del centro?

   - A. Que el centro siempre gana más que el pico fuera de muestra
   - B. Que no depende de la casualidad de una celda: en una meseta real cuesta poco o nada frente al pico, y en el ruido revela que no hay meseta (la vecindad del centro vale 0,10)
   - C. Que hay que elegir siempre la media lenta más larga
   - D. Que evita los costes de transacción

   *Respuesta B: Que no depende de la casualidad de una celda: en una meseta real cuesta poco o nada frente al pico, y en el ruido revela que no hay meseta (la vecindad del centro vale 0,10).* En una meseta real, pico y centro dan resultados parecidos fuera de muestra; aquí el centro incluso gana, porque fuera de muestra mandan las medias lentas largas y el centro cae en esa zona, pero eso no es una regla. Lo que la regla da siempre es una alarma: si el centro es mediocre, el pico era una casualidad.

5. El cruce 30/300 da Sharpe positivo en 21 de los 24 futuros, pero −0,03 en las divisas al contado de `fx2_carry`. ¿Cómo se interpreta?

   - A. La estrategia no es robusta: hay que descartarla
   - B. Las divisas de `fx2_carry` no llevan el momentum plantado: la robustez entre activos se exige donde la hipótesis dice que está el efecto, y un resultado nulo fuera de ese dominio es coherente con ella
   - C. Hay que optimizar otros parámetros para las divisas
   - D. Las divisas al contado tienen más costes

   *Respuesta B: Las divisas de `fx2_carry` no llevan el momentum plantado: la robustez entre activos se exige donde la hipótesis dice que está el efecto, y un resultado nulo fuera de ese dominio es coherente con ella.* Una prueba de robustez tiene sentido cuando la hipótesis predice qué debe pasar. El README del nivel 2 planta el momentum en los futuros y no en el contado de divisas; que la regla no funcione allí no la invalida. Lo que sí la invalidaría es que fallara en la mayoría de los futuros.

## Ideas clave

- Una superficie de parámetros se lee como un mapa: importa la forma alrededor del máximo, no el máximo.
- En los futuros con efecto plantado, las 64 combinaciones dan Sharpe positivo (0,60 a 1,12) y las vecinas del pico promedian el 97 % de su valor; en el ruido, el pico de 0,57 tiene vecinas que promedian 0,01 y solo el 41 % de las celdas es positivo.
- La rugosidad (salto medio entre celdas contiguas dividido por la desviación típica de la rejilla) vale 0,47 en la meseta y 0,94 en el ruido, cerca del 1,13 de una superficie de ruido blanco.
- Regla práctica: elegir el centro de la meseta (el máximo de la superficie suavizada, lejos de los bordes) y ampliar la rejilla si queda pegado a uno. Fuera de muestra, en el ruido el pico cae de 0,63 a 0,13; en los futuros, pico y centro dan 0,79 y 1,23.
- La suavidad es necesaria pero no suficiente: parámetros vecinos están muy correlados (0,77 de media en los futuros). La prueba siguiente es la robustez entre mercados, sectores y periodos.
- La robustez entre activos se exige donde la hipótesis predice el efecto: 21 de 24 futuros positivos; en las divisas al contado, sin momentum plantado, la misma regla da −0,03.

## Recursos

### Research

- [Estacionalidad Intradía - Optimización](https://pythonparatrading.com/estacionalidad-intradia-optimizacion/) · Research-0027 · 7 min
- [Primer Sistema de Trading - Cruce de Medias](https://pythonparatrading.com/primer-sistema-cruce-de-medias/) · Research-0018 · 7 min

### Documentación

- [pandas: DataFrame.rolling (medias móviles de la rejilla)](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.rolling.html) · pandas.pydata.org

### Libros

- [Bailey, D. H., Borwein, J., López de Prado, M. y Zhu, Q. J. (2016): The probability of backtest overfitting. The Journal of Computational Finance](https://doi.org/10.21314/JCF.2016.322) · doi.org
- [Harvey, C. R. y Liu, Y. (2015): Backtesting. The Journal of Portfolio Management 42(1), 13-28](https://doi.org/10.3905/jpm.2015.42.1.013) · doi.org
- [Moskowitz, T., Ooi, Y. H. y Pedersen, L. H. (2012): Time series momentum. Journal of Financial Economics 104(2), 228-250](https://doi.org/10.1016/j.jfineco.2011.11.003) · doi.org

### Datos

- [Datos del nivel 2: futuros continuos, 24 mercados, 2000-2025 (Parquet)](https://pythonparatrading.com/curso/datos/nivel-2/futuros2_continuos.parquet) · Descarga · futuros2_continuos.parquet

Lección anterior: [2.5.5 · Monte Carlo: remuestreo de trades, bootstrap de bloques y permutaciones de señales](https://pythonparatrading.com/curso-python-trading/nivel-2/monte-carlo-bootstrap-y-permutaciones/)

Lección siguiente: [2.5.7 · Datos sintéticos para pruebas de estrés: GBM, GARCH y bootstrapping](https://pythonparatrading.com/curso-python-trading/nivel-2/datos-sinteticos-para-pruebas-de-estres/)
