---
title: "5.1.3 Métricas económicas para modelos de trading"
description: "Por qué el acierto, el AUC y el R² engañan; IC, ICIR, acierto ponderado, Sharpe de la señal tras costes y calibración, medidos en el panel del nivel."
url: "https://pythonparatrading.com/curso-python-trading/nivel-5/metricas-economicas-para-modelos-de-trading/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-5/metricas-economicas-para-modelos-de-trading.png"
---

# 5.1.3 · Métricas económicas para modelos de trading: más allá del acierto

> Un modelo de trading no se juzga por cuántas veces acierta, sino por el dinero que deja la cartera que se construye con él, tras costes. En el panel del nivel, la señal con peor acierto es la que más gana neta, y un LightGBM con un AUC por encima de 0,5 da probabilidades peores que una moneda. Esta lección mide cada métrica sobre las mismas señales y enseña cuál usar para cada decisión.

- Curso: [Nivel 5: Machine learning y deep learning (Keras/PyTorch)](https://pythonparatrading.com/curso-python-trading/nivel-5/)
- Módulo: 5.1 · Fundamentos de ML aplicados a finanzas
- Duración: 65 min
- Requisitos: [2.4.7 · Modelado de costes: comisiones, horquilla, impacto, financiación y préstamo de títulos](https://pythonparatrading.com/curso-python-trading/nivel-2/modelado-de-costes/), [2.6.1 · CAGR, Sharpe, Sortino, Calmar, Omega, drawdown y Ulcer Index](https://pythonparatrading.com/curso-python-trading/nivel-2/metricas-de-rentabilidad-y-riesgo/), [4.2.2 · Criterio de Kelly y volatility targeting: cuánto arriesgar](https://pythonparatrading.com/curso-python-trading/nivel-4/kelly-y-volatility-targeting/), [5.1.1 · Por qué el machine learning financiero es distinto](https://pythonparatrading.com/curso-python-trading/nivel-5/por-que-el-ml-financiero-es-distinto/), [5.1.2 · Aprendizaje supervisado, no supervisado y por refuerzo; pipelines de scikit-learn](https://pythonparatrading.com/curso-python-trading/nivel-5/aprendizaje-supervisado-y-pipelines-de-scikit-learn/)

## Al terminar esta lección

- Explicar con datos por qué el acierto, el AUC y el R² no ordenan bien los modelos de trading.
- Calcular el IC semanal, el ICIR y la amplitud efectiva, y relacionarlos con la ley fundamental de la gestión activa.
- Construir la cartera larga-corta por deciles de una señal y medir su Sharpe bruto, su rotación, su Sharpe neto y su coste de equilibrio.
- Reducir la rotación suavizando la señal y medir el efecto en el Sharpe neto.
- Evaluar la calibración de un clasificador con un diagrama de fiabilidad, Brier y log loss, recalibrarlo y conectar el error con el dimensionamiento de Kelly.

## La pregunta que contesta cada métrica

Las métricas habituales del ML contestan preguntas de estadística: ¿cuántas veces acierta la clase?, ¿cuánto se
equivoca en media al cuadrado?, ¿ordena bien positivos y negativos? Un sistema de trading hace otra pregunta: **¿cuánto
dinero deja la cartera que se construye con estas predicciones, después de pagar por construirla?** Las dos familias
de preguntas no ordenan los modelos igual, y el resto de la lección lo mide con seis señales sobre el mismo panel y el
mismo periodo.

| Métrica | Qué mide | Qué ignora |
|---|---|---|
| Acierto | Fracción de signos (o clases) correctos | Magnitudes, clase mayoritaria, costes |
| AUC | Probabilidad de ordenar bien un positivo frente a un negativo | Magnitudes, escala, costes |
| R², error cuadrático | Error de la predicción en sus unidades | Que para ordenar solo cuenta el orden |
| IC, ICIR | Calidad del orden transversal y su estabilidad | Costes y rotación |
| Sharpe de la señal, neto | Rendimiento por unidad de riesgo de la cartera tras costes | Capacidad, colas (se miran aparte) |
| Brier, log loss, fiabilidad | Si las probabilidades significan lo que dicen | El orden (un modelo calibrado puede no ordenar nada) |

## El acierto engaña: la clase mayoritaria

El primer problema del acierto es su referencia. Los rendimientos diarios del activo `indice` de `vol5_diario`
tienen deriva positiva y, por construcción, son **impredecibles**: su media condicional es constante (el README del
nivel lo usa como control). Un clasificador que siempre dice «sube»:

```python
import numpy as np
import pandas as pd
from sklearn.metrics import accuracy_score, balanced_accuracy_score

v = pd.read_parquet("vol5_diario.parquet")
ind = v[(v["activo"] == "indice") & (v["fecha"] >= "2018-01-01")]
sube = (ind["rendimiento"] > 0).astype(int)
siempre = np.ones(len(sube), dtype=int)
print(f"Acierto: {accuracy_score(sube, siempre):.1%}")
print(f"Acierto equilibrado: "
      f"{balanced_accuracy_score(sube, siempre):.1%}")
```
```output
Acierto: 56.6%
Acierto equilibrado: 50.0%
```

Un 56,6 % de acierto sin ninguna información: es la frecuencia de días al alza. Cualquier clasificador direccional
se compara con esa cifra, no con el 50 %. El **acierto equilibrado** (la media del acierto en cada clase) quita el
efecto: un clasificador constante da exactamente 0,5. En el panel la etiqueta es «por encima de la mediana de la
semana», que está equilibrada por construcción, y el problema se traslada a la magnitud y los costes.

## Seis señales, seis rankings distintos

Las seis señales se evalúan en 2019-2025 sobre `panel5`:

- **oráculo**: el rendimiento esperado verdadero (`alfa_verdadero`), el techo de la lección 5.1.1.
- **ridge**: una ridge con las 21 features en rango, entrenada con 2012-2018 (como en la lección 5.1.1).
- **baseline**: la suma de cuatro rangos con el signo de la literatura (lección 5.1.2).
- **value**, **ey** y **reversión**: features sueltas (`valor_bp`, `ey` y `ret_1s` con el signo cambiado).

```python
from sklearn.linear_model import Ridge

f = pd.read_parquet("panel5_features.parquet")
et = pd.read_parquet("panel5_etiquetas.parquet")
ver = pd.read_parquet("panel5_verdad.parquet")
d = (f.merge(et[["fecha", "ticker", "ret_sig_semana"]],
             on=["fecha", "ticker"])
      .merge(ver[["fecha", "ticker", "alfa_verdadero"]],
             on=["fecha", "ticker"])
      .dropna(subset=["ret_sig_semana"]).reset_index(drop=True))
g = d.groupby("fecha")
d["y"] = d["ret_sig_semana"] - g["ret_sig_semana"].transform("mean")
d["arriba"] = (d["ret_sig_semana"]
               > g["ret_sig_semana"].transform("median")).astype(int)
fuera = ("fecha", "ticker", "sector", "revision_analistas")
feats = [c for c in f.columns if c not in fuera]
X = g[feats].rank(pct=True).sub(0.5).fillna(0)
ent = (d["fecha"] < "2019-01-01").to_numpy()
pru = (d["fecha"] >= "2019-01-08").to_numpy()

rk = g[["mom_12_1", "ret_1s", "valor_bp", "sue"]].rank(pct=True)
d["oraculo"] = d["alfa_verdadero"]
d["ridge"] = Ridge(alpha=10.0).fit(X[ent], d.loc[ent, "y"]).predict(X)
d["baseline"] = (rk["mom_12_1"] - rk["ret_1s"] + rk["valor_bp"]
                 + rk["sue"])
d["value"], d["ey_"], d["reversion"] = (d["valor_bp"], d["ey"],
                                        -d["ret_1s"])
senales = ["oraculo", "ridge", "baseline", "value", "ey_",
           "reversion"]
t = d[pru].reset_index(drop=True)
print(t["fecha"].nunique(), "semanas,", len(t), "filas")
```
```output
364 semanas, 145600 filas
```

Primero, las métricas de clasificación. La predicción de cada señal se convierte en «por encima de la mediana de la
semana» y se compara con la etiqueta binaria. El **acierto ponderado** pesa cada acierto por el tamaño del
movimiento, $|y_i|$: un modelo que acierta los movimientos grandes y falla los pequeños sale mejor parado que en el
acierto simple. El **AUC** usa la señal continua y mide la probabilidad de que una acción de la mitad buena tenga
una señal mayor que una de la mitad mala:

```python
from sklearn.metrics import roc_auc_score


def clasificacion(s, col):
    alta = s[col] > s.groupby("fecha")[col].transform("median")
    ok = alta.astype(int) == s["arriba"]
    return {"acierto": ok.mean(),
            "acierto_pond": (s["y"].abs() * ok).sum()
            / s["y"].abs().sum(),
            "auc": roc_auc_score(s["arriba"], s[col])}


pd.DataFrame({c: clasificacion(t, c) for c in senales}).T.round(4)
```
```output
           acierto  acierto_pond     auc
oraculo     0.5077        0.5108  0.5124
ridge       0.5061        0.5106  0.5087
baseline    0.5056        0.5086  0.5089
value       0.5041        0.5062  0.5055
ey_         0.5034        0.5051  0.5046
reversion   0.5034        0.5073  0.5052
```

Todas las señales están entre el 50,3 % y el 50,8 % de acierto: es la escala de la lección 5.1.1. El acierto ponderado
cambia el orden: la reversión sube del 50,34 % al 50,73 % porque acierta más en los movimientos grandes (una
caída fuerte de la semana anterior se corrige más), y adelanta al value y a `ey`. El AUC ordena casi igual que el
acierto. Ninguna de las tres métricas dice nada de cuánto cuesta operar cada señal, que es lo que decide el resultado.

## R² y error cuadrático: escala frente a orden

Un modelo de regresión se evalúa por defecto con el R² o el error cuadrático. Para ordenar acciones, la escala de la
predicción es irrelevante; para el R², es casi todo:

```python
from sklearn.metrics import r2_score


def ic_semanal(s, col, y="y"):
    r = s.groupby("fecha")[[col, y]].rank()
    r = r - r.groupby(s["fecha"]).transform("mean")
    num = (r[col] * r[y]).groupby(s["fecha"]).sum()
    den = ((r[col] ** 2).groupby(s["fecha"]).sum()
           * (r[y] ** 2).groupby(s["fecha"]).sum()) ** 0.5
    return num / den


t["oraculo_x5"] = 5 * t["oraculo"]
for c in ["oraculo", "oraculo_x5", "ridge"]:
    print(f"{c:11s} R² {r2_score(t['y'], t[c]):+.3%}   "
          f"IC {ic_semanal(t, c).mean():.4f}")
```
```output
oraculo     R² +0.044%   IC 0.0254
oraculo_x5  R² -2.531%   IC 0.0254
ridge       R² +0.040%   IC 0.0190
```

El oráculo explica un 0,044 % de la varianza de la etiqueta. Multiplicado por 5 tiene **exactamente** el mismo IC y
produciría la misma cartera por deciles, pero su R² cae a −2,5 %: peor que predecir siempre cero. La ridge tiene un
R² parecido al del oráculo y un IC menor. El R² sirve cuando la magnitud de la predicción se usa tal cual (para
dimensionar, para la volatilidad de la lección 5.8.5); para elegir entre señales de ordenación, confunde.

## IC, ICIR y la ley fundamental

El **IC** semanal mide la calidad del orden; el **ICIR**, su media dividida por su desviación entre semanas, mide su
estabilidad. Anualizado ($\times\sqrt{52}$) es comparable a un Sharpe: si la cartera pesa cada acción en proporción a
su rango, su rendimiento semanal es aproximadamente proporcional al IC de esa semana por la dispersión de los
rendimientos.

La **ley fundamental de la gestión activa** (Grinold, 1989) da una cota del ratio de información con $N$ apuestas
independientes por periodo:

$$\mathrm{IR} \approx \mathrm{IC}\cdot\sqrt{\text{amplitud}}, \qquad
\text{amplitud} = N \times \text{periodos por año}.$$

La celda la contrasta con el ICIR medido y calcula la **amplitud efectiva**: si las apuestas fueran independientes,
la desviación del IC semanal sería $1/\sqrt{N}$; despejando $N$ de la desviación medida sale cuántas apuestas
independientes hay de verdad cada semana.

```python
filas = {}
for c in senales:
    ic = ic_semanal(t, c)
    n = t.groupby("fecha")[c].count().mean()
    filas[c] = {"ic": ic.mean(), "desv_ic": ic.std(),
                "icir_anual": ic.mean() / ic.std() * np.sqrt(52),
                "ley_fund": ic.mean() * np.sqrt(n * 52),
                "amplitud_efectiva": 1 / ic.std() ** 2}
pd.DataFrame(filas).T.round(3)
```
```output
              ic  desv_ic  icir_anual  ley_fund  amplitud_efectiva
oraculo    0.025    0.100       1.829     3.668             99.437
ridge      0.019    0.088       1.562     2.737            130.271
baseline   0.018    0.082       1.567     2.560            149.781
value      0.010    0.072       1.041     1.495            194.029
ey_        0.009    0.062       1.093     1.365            256.640
reversion  0.016    0.103       1.099     2.258             94.724
```

Con 400 acciones, la ley fundamental promete al oráculo un ratio de información de 3,7. El ICIR medido es 1,83: la
mitad. La desviación del IC entre semanas es de 0,10, el doble de lo que darían 400 apuestas independientes
($1/\sqrt{400} = 0{,}05$), y equivale a unas 100. Las 400 acciones comparten mercado, sectores y features lentas: la
amplitud de la ley es de **decisiones independientes**, no de activos, y contarla mal infla cualquier expectativa.
El ICIR ya incluye esa dependencia, por eso es la medida de la calidad de una señal antes de costes.

## El Sharpe de la señal tras costes

La medida que manda es la cartera. La convención del nivel (README de los datos): cada semana, larga en el decil
alto de la señal y corta en el bajo, equiponderada, con un nominal bruto de 2 (1 largo y 1 corto); rendimientos
simples; rotación = suma de los cambios de peso en valor absoluto; coste = 10 pb por lado sobre la rotación (lección
2.4.7). El **coste de equilibrio** es el coste por lado que deja el rendimiento neto en cero: rendimiento bruto medio
dividido por rotación media.

```python
def pesos_deciles(s, col):
    q = s.groupby("fecha")[col].rank(pct=True)
    w = pd.Series(np.select([q > 0.9, q <= 0.1], [1.0, -1.0], 0.0),
                  index=s.index)
    w = 2 * w / w.abs().groupby(s["fecha"]).transform("sum")
    return (s[["fecha", "ticker"]].assign(w=w)
            .pivot(index="fecha", columns="ticker", values="w")
            .fillna(0.0))


R = t.pivot(index="fecha", columns="ticker", values="ret_sig_semana")


def cartera(W, coste_pb=10):
    bruto = (W * R).sum(axis=1)
    rot = W.diff().abs().sum(axis=1)
    rot.iloc[0] = W.iloc[0].abs().sum()
    neto = bruto - coste_pb / 1e4 * rot
    sr = lambda x: x.mean() / x.std() * np.sqrt(52)
    return {"bruto_anual": bruto.mean() * 52, "sharpe_bruto": sr(bruto),
            "rotacion_sem": rot.mean(), "sharpe_neto": sr(neto),
            "equilibrio_pb": 1e4 * bruto.mean() / rot.mean()}


W = {c: pesos_deciles(t, c) for c in senales}
res = pd.DataFrame({c: cartera(W[c]) for c in senales}).T
res["acierto"] = [clasificacion(t, c)["acierto"] for c in senales]
res.round(3)
```
```output
           bruto_anual  sharpe_bruto  rotacion_sem  sharpe_neto  equilibrio_pb  acierto
oraculo          0.167         1.667         2.649        0.287         12.089    0.508
ridge            0.129         1.404         2.541       -0.029          9.797    0.506
baseline         0.113         1.331         2.030        0.090         10.726    0.506
value            0.076         1.094         0.105        1.015        139.932    0.504
ey_              0.061         0.889         0.102        0.810        114.183    0.503
reversion        0.133         1.295         3.420       -0.435          7.486    0.503
```

La figura de la cabecera cruza las dos columnas extremas de la tabla: el acierto y el Sharpe neto. El orden se
invierte casi por completo:

- El **oráculo** tiene el mejor acierto (50,77 %) y el mejor Sharpe bruto (1,67), pero su cartera rota un 265 %
  semanal: aguanta 12 pb por lado y con 10 pb queda en 0,29.
- La **ridge** y la **baseline** dan Sharpes brutos de 1,40 y 1,33 y netos de −0,03 y 0,09: la misma historia con
  menos señal. Es lo que el README del nivel da para 2015-2025.
- La **reversión** de una semana tiene un Sharpe bruto de 1,30 y la peor rotación (342 %): con 10 pb pierde dinero
  (−0,43). Su coste de equilibrio, 7 pb, es menor que la horquilla de muchas acciones reales.
- El **value** y **ey** tienen aciertos de los más bajos (50,41 % y 50,34 %) y los **mejores Sharpes netos** (1,01 y
  0,81). Sus carteras cambian un 10 % a la semana: aguantan 140 y 114 pb por lado.

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

Un Sharpe neto de 1,01 con una sola feature durante siete años no es una recomendación: es un resultado sobre datos
sintéticos en los que el value tiene un efecto plantado. En datos reales, el value es una de las anomalías más
conocidas y más estudiadas (lección 5.1.1: lo publicado pierde rendimiento), y siete años dan un error típico del
Sharpe de unos $1/\sqrt{7} \approx 0{,}38$. Lo que sí es general es el mecanismo: entre señales parecidas, la que
rota menos se queda con más del rendimiento bruto.

</aside>

### Suavizar para rotar menos

Si una señal tiene una parte lenta (lección 5.1.1: el oráculo conserva un IC de 0,011 con semanas de retraso), se
puede cambiar algo de IC por mucha menos rotación. Lo más sencillo es una media exponencial de los rangos de la
señal en las últimas semanas, que solo usa información pasada:

```python
def suavizar(col, vida_media=2):
    p = d.pivot(index="fecha", columns="ticker", values=col)
    s = p.rank(axis=1, pct=True).ewm(halflife=vida_media).mean()
    return s.loc[t["fecha"].unique()]


filas = {}
for c in ["oraculo", "ridge", "baseline", "reversion"]:
    S = suavizar(c)
    larga = S.stack().rename("s").reset_index()
    ts = t[["fecha", "ticker"]].merge(larga, on=["fecha", "ticker"])
    filas[c + "_suave"] = {**cartera(pesos_deciles(ts, "s")),
                           "ic": ic_semanal(ts.assign(y=t["y"]),
                                            "s").mean()}
pd.DataFrame(filas).T.round(3)
```
```output
                 bruto_anual  sharpe_bruto  rotacion_sem  sharpe_neto  equilibrio_pb     ic
oraculo_suave          0.120         1.184         1.016        0.661         22.658  0.016
ridge_suave            0.103         1.231         0.999        0.613         19.913  0.012
baseline_suave         0.091         1.202         0.626        0.771         27.878  0.013
reversion_suave        0.059         0.607         2.104       -0.514          5.417  0.008
```

*Fig. — Sharpe neto anual de la cartera larga-corta por deciles según el coste por lado, 2019-2025: las señales que rotan mucho cruzan el cero enseguida.* Cuatro rectas descendentes desde coste cero. Reversión: de 1,30 a cero hacia 7,5 pb. Oráculo: de 1,67 a cero hacia 12 pb. Oráculo suavizado (destacado): de 1,18 a cero hacia 23 pb. Value: casi plana, de 1,09 a 0,90 con 25 pb.

Con una vida media de dos semanas, el IC del oráculo baja de 0,025 a 0,016 y su Sharpe bruto de 1,67 a 1,18, pero la
rotación cae de 265 % a 102 % semanal: el coste de equilibrio sube de 12 a 23 pb y el Sharpe neto, de 0,29 a 0,66. La
ridge pasa de −0,03 a 0,61 y la baseline, de 0,09 a 0,77. La reversión, cuyo IC está todo en la primera semana, no
tiene parte lenta que conservar. La figura dibuja el Sharpe neto según el coste por lado: las rectas de las señales
rápidas caen deprisa y cruzan el cero entre 7 y 12 pb; las suavizadas y el value aguantan mucho más.

Optimizar el suavizado (la vida media, los umbrales de los deciles) con los mismos datos de prueba es otro
experimento que cuenta para el Deflated Sharpe (lecciones 2.5.4 y 5.4.4). Aquí se ha probado un único valor, fijado
de antemano; la lección 4.1.5 trata la rotación como restricción de la optimización.

## Calibración: cuando la probabilidad decide el tamaño

Algunos modelos no se usan para ordenar sino para decidir **cuánto** apostar: el meta-labeling de la lección 5.3.3
dimensiona cada operación con la probabilidad de acierto del modelo secundario, y Kelly (lección 4.2.2) apuesta
$f = 2p - 1$ en una apuesta simétrica. Ahí no basta con que $p$ ordene bien: tiene que **significar** lo que dice. Un
modelo está calibrado si, de las veces que dice 0,6, acierta el 60 %.

El experimento: dos clasificadores que predicen si una acción quedará por encima de la mediana de la semana
siguiente, entrenados con 2012-2016. Una regresión logística y un LightGBM bastante flexible (63 hojas, 300 árboles).
Con 2017-2018, que ninguno ha visto, se recalibra el LightGBM con una regresión **isotónica** (una función monótona
de su probabilidad) mediante `CalibratedClassifierCV` y `FrozenEstimator`, que lo deja tal cual y solo ajusta la
recalibración. Se evalúa todo en 2019-2025:

```python
import lightgbm as lgb
from sklearn.calibration import CalibratedClassifierCV
from sklearn.frozen import FrozenEstimator
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import brier_score_loss, log_loss

a = (d["fecha"] < "2017-01-01").to_numpy()
c = ((d["fecha"] >= "2017-01-13")
     & (d["fecha"] < "2019-01-01")).to_numpy()
y = d["arriba"]
logit = LogisticRegression(C=1.0, max_iter=1000).fit(X[a], y[a])
gbm = lgb.LGBMClassifier(
    n_estimators=300, learning_rate=0.1, num_leaves=63,
    min_child_samples=20, random_state=0, n_jobs=4,
    deterministic=True, force_row_wise=True, verbose=-1,
).fit(X[a], y[a])
recal = CalibratedClassifierCV(FrozenEstimator(gbm),
                               method="isotonic").fit(X[c], y[c])

yp, fiab, tabla = y[pru].to_numpy(), {}, {}
for nombre, m in [("logistica", logit), ("lgbm", gbm),
                  ("lgbm_recal", recal)]:
    p = m.predict_proba(X[pru])[:, 1]
    tramo = pd.qcut(p, 10, labels=False, duplicates="drop")
    fiab[nombre] = pd.DataFrame({"p": p, "obs": yp}).groupby(
        tramo).mean()
    tabla[nombre] = {"auc": roc_auc_score(yp, p),
                     "brier": brier_score_loss(yp, p),
                     "log_loss": log_loss(yp, p),
                     "kelly_medio": np.abs(2 * p - 1).mean()}
tabla["moneda"] = {"auc": 0.5, "brier": 0.25,
                   "log_loss": np.log(2), "kelly_medio": 0.0}
print(pd.DataFrame(tabla).T.round(4))
print(fiab["lgbm"].T.round(3))
print(fiab["logistica"].T.round(3))
```
```output
               auc   brier  log_loss  kelly_medio
logistica   0.5060  0.2500    0.6932       0.0196
lgbm        0.5006  0.2546    0.7028       0.1008
lgbm_recal  0.5019  0.2500    0.6944       0.0091
moneda      0.5000  0.2500    0.6931       0.0000
         0      1      2      3      4      5      6      7      8      9
p    0.373  0.438  0.463  0.480  0.494  0.507  0.521  0.538  0.563  0.622
obs  0.497  0.499  0.499  0.499  0.500  0.500  0.505  0.511  0.493  0.497
         0      1      2      3      4      5      6      7      8      9
p    0.478  0.487  0.492  0.495  0.499  0.502  0.505  0.509  0.513  0.520
obs  0.490  0.495  0.494  0.498  0.500  0.501  0.508  0.505  0.503  0.507
```

*Fig. — Diagrama de fiabilidad en 2019-2025: probabilidad media predicha por decil frente a la frecuencia observada de acabar por encima de la mediana, para una logística y un LightGBM.* Una diagonal discontinua marca la calibración perfecta. Los diez puntos del LightGBM (destacados) van de 0,37 a 0,62 en probabilidad predicha, pero su frecuencia observada se queda plana entre 0,49 y 0,51: sobreconfianza. Los de la logística se agrupan entre 0,48 y 0,52, cerca del centro.

El diagrama de fiabilidad (la figura) pone en el eje horizontal la probabilidad media predicha en cada decil y en el
vertical la frecuencia observada; un modelo calibrado cae sobre la diagonal. El LightGBM predice entre 0,37 y 0,62 de
media por decil y la frecuencia observada es 0,49-0,51 en todos: está **sobreconfiado**. Su AUC (0,501) apenas se
separa de 0,5, y su Brier (0,2546) y su log loss (0,7028) son **peores que los de una moneda** (0,25 y 0,6931).
Dimensionado con Kelly, apostaría de media un 10 % del capital por posición sin ninguna ventaja. La logística ordena
algo mejor (AUC 0,506) y sus probabilidades se mueven en un rango estrecho (0,48-0,52), y aun así su log loss
(0,6932) no mejora a la de la moneda: la frecuencia observada solo va de 0,49 a 0,51 y, con esta relación
señal/ruido, una probabilidad por acción distinta de 0,5 apenas aporta información. Recalibrado, el LightGBM se comprime alrededor de 0,5 y su tamaño medio de Kelly baja al 0,9 %; su log
loss sigue algo por encima de la moneda (0,6944). La recalibración no le da señal, pero le quita la falsa confianza.

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

La recalibración necesita datos que el modelo no haya visto (aquí, 2017-2018) y tiene que repetirse cuando el modelo
se reentrena. Calibrar con los datos de entrenamiento no corrige nada: un modelo sobreajustado está bien calibrado
sobre los datos que memorizó. Y la calibración no es estable si la relación cambia (lección 5.10.2): una probabilidad
calibrada en calma puede no serlo en un rebote.

</aside>

## Qué métrica para qué decisión

La métrica se elige por el uso que se va a dar a la predicción:

```mermaid
flowchart TB
  U{¿Para qué se usa?} --> O[Ordenar activos]
  U --> T[Dimensionar]
  U --> F[Filtrar operaciones]
  O --> IC[IC e ICIR]
  T --> CA[Calibración]
  F --> PR[Precisión y P&L]
  IC --> SN[Sharpe neto]
  CA --> SN
  PR --> SN
  SN --> DSR[Deflated Sharpe]
```

- **Ordenar activos** (carteras transversales): IC semanal, su desviación e ICIR para comparar señales; Sharpe de la
  cartera por deciles, bruto y neto, con su rotación y su coste de equilibrio.
- **Dimensionar** (meta-labeling, Kelly, volatility targeting): calibración (Brier, log loss, diagrama de fiabilidad)
  además del orden; si la predicción es una volatilidad, QLIKE o error en logaritmos (lección 5.8.5).
- **Filtrar operaciones** de un sistema primario: precisión de las operaciones que se dejan pasar y P&L medio por
  operación frente al del sistema sin filtrar (lección 5.3.3).
- **Siempre**: el Sharpe neto de la estrategia completa, con su error típico, y descontado por el número de
  experimentos con el Deflated Sharpe (lecciones 2.5.4 y 5.4.4). Y las colas y la peor racha, que ningún Sharpe ve
  (lección 2.6.1).

Durante el entrenamiento se puede optimizar una pérdida estadística (error cuadrático, entropía cruzada), que es
estable y diferenciable; la lección 5.6.4 prueba una pérdida basada en el Sharpe. Pero la **elección** entre modelos,
hiperparámetros y variantes se hace con las métricas económicas, sobre datos que el modelo no ha visto.

## Ejercicios

1. Calcula el Sharpe neto de la cartera del oráculo y del value con costes de 0, 5, 10, 20 y 40 pb por lado. ¿A
   partir de qué coste el value supera al oráculo?

**Solución.**

```python
costes = [0, 5, 10, 20, 40]
pd.DataFrame({c: [cartera(W[c], k)["sharpe_neto"] for k in costes]
              for c in ["oraculo", "value"]},
             index=pd.Index(costes, name="coste_pb")).round(2)
```
```output
          oraculo  value
coste_pb
0            1.67   1.09
5            0.98   1.05
10           0.29   1.01
20          -1.08   0.94
40          -3.78   0.78
```

Sin costes, el oráculo gana con claridad (1,67 frente a 1,09). Con 5 pb por lado el value ya es mejor (1,05 frente a
0,98), y a 10 pb la diferencia es enorme (1,01 frente a 0,29). A 40 pb el oráculo pierde mucho (−3,78) y el value
apenas nota el coste (0,78). El cruce está por debajo de 5 pb por lado: menos de lo que cuesta operar una acción de
mediana capitalización en un mercado real.

2. Repite la tabla de IC e ICIR con la etiqueta `ret_sig_21d` (rendimiento de 21 sesiones) para el oráculo y el value.
   ¿Por qué no conviene calcular su significación con las semanas como si fueran independientes?

**Solución.**

```python
y21 = et[["fecha", "ticker", "ret_sig_21d"]]
t21 = t.merge(y21, on=["fecha", "ticker"]).dropna(
    subset=["ret_sig_21d"])
for c in ["oraculo", "value"]:
    ic = ic_semanal(t21, c, "ret_sig_21d")
    ac1 = ic.autocorr(1)
    print(f"{c:8s} IC {ic.mean():.4f}  ICIR anual "
          f"{ic.mean() / ic.std() * np.sqrt(52):.2f}  "
          f"autocorrelación del IC {ac1:.2f}")
```
```output
oraculo  IC 0.0207  ICIR anual 1.61  autocorrelación del IC 0.47
value    IC 0.0209  ICIR anual 2.04  autocorrelación del IC 0.76
```

Con la etiqueta de 21 sesiones, las etiquetas de semanas consecutivas se solapan en unas tres cuartas partes, y el
IC de una semana se parece mucho al de la siguiente: la autocorrelación de orden 1 de la serie de IC lo mide. El ICIR
calculado como si las semanas fueran independientes sobrestima la significación; hace falta un error típico con
corrección por autocorrelación (Newey-West, lección 2.1.1) o usar solo semanas no solapadas.

3. Un proveedor te ofrece un modelo con «un 58 % de acierto direccional diario en el índice». ¿Qué tres preguntas le
   harías antes de mirar nada más?

**Solución.**

Primera: ¿cuál es la frecuencia de días al alza en el mismo periodo? Con la deriva de un índice, «siempre sube» ya da
un 55-57 %, así que un 58 % puede no aportar nada; pediría el acierto equilibrado o la comparación con esa referencia.
Segunda: ¿cuánto gana o pierde cuando acierta y cuando falla? El acierto no ve magnitudes: pediría el P&L de la
estrategia, el acierto ponderado por el tamaño del movimiento y el Sharpe neto con costes y la rotación. Tercera:
¿cómo se ha validado y cuántas variantes se probaron? Pediría el periodo fuera de muestra, el método de validación
(lecciones 5.4.1-5.4.3) y el número de experimentos para el Deflated Sharpe (5.4.4).

## Autoevaluación

1. Un clasificador que predice «sube» todos los días acierta el 56,6 % de los días del índice en 2018-2025. ¿Qué vale como predictor?

   - A. Mucho: acierta más que un modelo del 52 %
   - B. Depende del horizonte
   - C. Nada: es la frecuencia de días al alza (la deriva del índice); su acierto equilibrado es exactamente el 50 % y en estos datos el rendimiento es impredecible por construcción
   - D. Es una buena baseline para operar

   *Respuesta C: Nada: es la frecuencia de días al alza (la deriva del índice); su acierto equilibrado es exactamente el 50 % y en estos datos el rendimiento es impredecible por construcción.* El acierto se compara con la frecuencia de la clase mayoritaria, no con el 50 %. El acierto equilibrado (media del acierto en cada clase) quita el efecto: un clasificador constante da 0,5.

2. En 2019-2025, el value como señal suelta tiene uno de los peores aciertos de las seis señales (50,41 %) y el mejor Sharpe neto (1,01). ¿Por qué?

   - A. Por azar
   - B. Porque rota un 10 % semanal frente a 250-340 % de las demás: su coste de equilibrio es de 140 pb por lado, y con 10 pb casi no pierde nada
   - C. Porque el value tiene el mayor IC
   - D. Porque el acierto está mal calculado

   *Respuesta B: Porque rota un 10 % semanal frente a 250-340 % de las demás: su coste de equilibrio es de 140 pb por lado, y con 10 pb casi no pierde nada.* El acierto no ve ni la magnitud ni la rotación. El oráculo tiene más IC y más Sharpe bruto (1,67), pero su cartera se renueva cada semana y solo aguanta 12 pb por lado; con 10 pb queda en 0,29.

3. Multiplicar por 5 la predicción del oráculo cambia su R² fuera de muestra de +0,04 % a −2,5 %. ¿Qué pasa con su cartera larga-corta por deciles?

   - A. Nada: el orden de las acciones es el mismo, así que el IC y la cartera son idénticos. El R² castiga la escala, que en una cartera por rangos no importa
   - B. Pierde dinero
   - C. Gana cinco veces más
   - D. Su rotación se multiplica por 5

   *Respuesta A: Nada: el orden de las acciones es el mismo, así que el IC y la cartera son idénticos. El R² castiga la escala, que en una cartera por rangos no importa.* El R² mide el error cuadrático de la predicción en sus unidades. Para ordenar acciones solo cuenta el orden; para dimensionar posiciones sí cuenta la escala, y entonces lo que se mira es la calibración.

4. El IC semanal del oráculo es 0,025 y hay 400 acciones. La ley fundamental da un ratio de información de 0,025·√(400·52) ≈ 3,7, pero el ICIR anual medido es 1,8. ¿De dónde sale la diferencia?

   - A. De un error de cálculo
   - B. De los costes
   - C. De que el IC es de rangos
   - D. De que las 400 apuestas semanales no son independientes: comparten mercado, sectores y features. La desviación del IC entre semanas (≈ 0,1) corresponde a unas 100 apuestas independientes por semana, no a 400

   *Respuesta D: De que las 400 apuestas semanales no son independientes: comparten mercado, sectores y features. La desviación del IC entre semanas (≈ 0,1) corresponde a unas 100 apuestas independientes por semana, no a 400.* Con N apuestas independientes la desviación del IC sería ≈ 1/√N = 0,05. La medida es el doble, así que la amplitud efectiva es la cuarta parte. La amplitud de la ley fundamental es de decisiones independientes, no de activos.

5. Un LightGBM da probabilidades de que una acción quede por encima de la mediana entre 0,31 y 0,68; en esos tramos la frecuencia observada es siempre ≈ 0,50. Si se dimensiona con Kelly (f = 2p − 1), ¿qué pasa?

   - A. Nada: el AUC es mayor que 0,5
   - B. Se apuesta muy poco
   - C. Se apuesta lo correcto
   - D. Se apuesta de media un 10 % del capital por posición sin ventaja real: el modelo está sobreconfiado, su Brier y su log loss son peores que los de una moneda (0,2546 frente a 0,25)

   *Respuesta D: Se apuesta de media un 10 % del capital por posición sin ventaja real: el modelo está sobreconfiado, su Brier y su log loss son peores que los de una moneda (0,2546 frente a 0,25).* Las probabilidades no calibradas convierten ruido en tamaño. Recalibrado con 2017-2018 (isotónica), las probabilidades se comprimen alrededor de 0,5 y el tamaño medio de Kelly baja al 0,9 %.

## Ideas clave

- El acierto se compara con la clase mayoritaria y no ve magnitudes: el acierto ponderado por |rendimiento| y el acierto equilibrado lo corrigen en parte.
- El IC semanal (correlación de rangos) mide lo que importa para ordenar; su media dividida por su desviación, anualizada (ICIR), se parece al Sharpe bruto de la cartera larga-corta.
- La ley fundamental IR ≈ IC·√amplitud usa apuestas independientes: con 400 acciones correlacionadas, la amplitud efectiva medida es de unas 100 por semana.
- La métrica final es el Sharpe neto de la cartera: el oráculo pasa de 1,67 bruto a 0,29 neto con 10 pb por lado, y el value, con un acierto peor, da 1,01.
- El coste de equilibrio (rendimiento bruto / rotación) dice cuántos pb por lado aguanta una señal; suavizarla sube el del oráculo de 12 a 23 pb y su Sharpe neto de 0,29 a 0,66.
- Si la probabilidad decide el tamaño, se exige calibración (Brier, log loss, diagrama de fiabilidad): un modelo sobreconfiado apuesta sin ventaja.

## Recursos

### Research

- [Alpha Factors y Alpha Trading en Python](https://pythonparatrading.com/alpha-factors-y-alpha-trading-en-python/) · Research-0007 · 5 min

### Documentación

- [scikit-learn: Metrics and scoring](https://scikit-learn.org/stable/modules/model_evaluation.html) · scikit-learn.org
- [scikit-learn: Probability calibration](https://scikit-learn.org/stable/modules/calibration.html) · scikit-learn.org
- [scikit-learn: FrozenEstimator](https://scikit-learn.org/stable/modules/generated/sklearn.frozen.FrozenEstimator.html) · scikit-learn.org

### Libros

- [Grinold, R. C. (1989): The Fundamental Law of Active Management. The Journal of Portfolio Management 15(3), 30-37](https://doi.org/10.3905/jpm.1989.409211) · doi.org

### Datos

- [Datos del nivel 5: verdad del panel, con el rendimiento esperado de cada acción (Parquet)](https://pythonparatrading.com/curso/datos/nivel-5/panel5_verdad.parquet) · Descarga · panel5_verdad.parquet
- [Datos del nivel 5: rendimientos diarios y varianza realizada de seis activos (Parquet)](https://pythonparatrading.com/curso/datos/nivel-5/vol5_diario.parquet) · Descarga · vol5_diario.parquet

Lección anterior: [5.1.2 · Aprendizaje supervisado, no supervisado y por refuerzo; pipelines de scikit-learn](https://pythonparatrading.com/curso-python-trading/nivel-5/aprendizaje-supervisado-y-pipelines-de-scikit-learn/)

Lección siguiente: [5.2.1 · Features de precio, volumen y microestructura](https://pythonparatrading.com/curso-python-trading/nivel-5/features-de-precio-volumen-y-microestructura/)
