---
title: "2.6.2 Métricas de operaciones, alpha, beta y capacidad"
description: "Win rate, payoff, expectativa y profit factor; alpha y beta con errores HAC, information ratio, turnover y capacidad, sobre datos con efectos plantados."
url: "https://pythonparatrading.com/curso-python-trading/nivel-2/metricas-de-operaciones-alpha-beta-y-capacidad/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-2/metricas-de-operaciones-alpha-beta-y-capacidad.png"
---

# 2.6.2 · Win rate, payoff, expectativa, alpha, beta, information ratio, turnover y capacidad

> Las métricas de la lección 2.6.1 miran la curva de capital. Estas miran sus piezas: cada operación, la parte de la rentabilidad que se explica por el mercado y lo que cuesta mover la cartera. Con un control sin ventaja que acierta el 63 % de las veces y pierde dinero, y un placebo que pasa el listón de t = 2.

- Curso: [Nivel 2: Estrategias sistemáticas y backtesting riguroso](https://pythonparatrading.com/curso-python-trading/nivel-2/)
- Módulo: 2.6 · Métricas de rendimiento
- Duración: 65 min
- Requisitos: [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/), [1.4.4 · Universos sin sesgo de supervivencia: deslistados, fusiones y cambios de símbolo](https://pythonparatrading.com/curso-python-trading/nivel-1/sesgo-de-supervivencia-y-point-in-time/)

## Al terminar esta lección

- Construir una lista de operaciones a partir de las posiciones de un backtest y calcular win rate, payoff, expectativa, profit factor y win rate de equilibrio.
- Explicar con datos por qué el win rate solo no dice si una estrategia gana dinero (tendencia frente a carry frente a una reversión sin ventaja).
- Estimar alpha y beta frente a un índice con errores de Newey-West, y distinguir un efecto plantado de un placebo y de una confusión.
- Calcular information ratio, tracking error y appraisal ratio, y saber cuál responde a qué pregunta.
- Medir el turnover de una cartera y estimar su capacidad con la horquilla, el volumen diario y un modelo de impacto.

## De la curva a la lista de operaciones

La lección 2.6.1 resumió tres curvas de capital. Una curva es el resultado de muchas decisiones, y para saber de qué
están hechas hay que mirarlas una a una. Esta lección trabaja en tres niveles: las **operaciones** (cuánto se gana y se
pierde en cada una), la **cartera frente a un índice** (qué parte de la rentabilidad es mercado y qué parte no) y la
**ejecución** (cuánto rota la cartera y cuánto capital admite).

```mermaid
flowchart TB
  P[Posiciones diarias] --> O[Operaciones]
  O --> WR[Win rate p]
  O --> WL[W y L medias]
  WR --> E[Expectativa]
  WL --> E
  WL --> PF[Profit factor]
  P --> R[Rentabilidad]
  R --> AB[Alfa y beta]
  P --> T[Turnover]
  T --> C[Capacidad]
```

Se empieza por reconstruir las dos estrategias de la lección 2.6.1 sobre los datos sintéticos del nivel 2: TSMOM en 24
futuros y el carry de diez divisas, del 1 de febrero de 2005 a 2025. Los efectos están plantados (un término de momentum
en cada futuro y un carry que gana el 70 % del diferencial con cinco crashes): las cifras enseñan a medir, no dicen que
esas estrategias funcionen fuera de estos datos.

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

fut = pd.read_parquet("futuros2_continuos.parquet")
px = fut.pivot(index="fecha", columns="mercado",
               values="cierre_ajustado")
r_fut = px.pct_change()
vol = r_fut.ewm(span=60, min_periods=60).std() * np.sqrt(252)
senal = np.sign(px / px.shift(252) - 1)
peso = (senal * 0.40 / px.shape[1] / vol).shift(1)

fx = pd.read_parquet("fx2_carry.parquet")
tabla = lambda c: fx.pivot(index="fecha", columns="divisa", values=c)
spot, dif = tabla("spot"), tabla("tipo_divisa") - tabla("tipo_usd")
dias = spot.index.to_series().diff().dt.days
interes = dif.shift(1) / 100 * dias.to_numpy()[:, None] / 360
r_fx = spot.pct_change() + interes
fin_mes = dif.index.to_series().groupby(dif.index.to_period("M")).max()
rango = dif.loc[fin_mes].rank(axis=1)
w_fx = ((rango >= 8).astype(float) - (rango <= 3).astype(float)) / 3
w_fx = w_fx.reindex(dif.index).ffill().shift(1)

inicio = "2005-02-01"
peso, r_fut = peso.loc[inicio:], r_fut.loc[inicio:]
w_fx, r_fx = w_fx.loc[inicio:], r_fx.loc[inicio:]
print(peso.shape, w_fx.shape)
```
```output
(5263, 24) (5457, 10)
```

## Win rate, payoff y expectativa

Una **operación** de TSMOM es una racha de posición con el mismo signo en un mercado: empieza cuando la señal cambia de
signo y termina cuando vuelve a cambiar. Su resultado es la suma de lo que aporta a la cartera cada día, en fracción del
capital. El tamaño de cada posición cambia con la volatilidad, pero la operación es la decisión de estar largo o corto.

```python
def operaciones(peso, r):
    """Una operación: una racha del mismo signo en un mercado."""
    aporte = peso * r       # rendimiento que aporta a la cartera
    filas = []
    for c in peso.columns:
        lado = np.sign(peso[c]).fillna(0)
        racha = (lado != lado.shift()).cumsum()
        d = pd.DataFrame({"lado": lado, "pnl": aporte[c],
                          "racha": racha})
        d = d[d["lado"] != 0].reset_index()
        t = d.groupby("racha").agg(
            entrada=("fecha", "first"), salida=("fecha", "last"),
            lado=("lado", "first"), pnl=("pnl", "sum"),
            sesiones=("pnl", "size"))
        filas.append(t.assign(mercado=c))
    return pd.concat(filas, ignore_index=True)

op_tsmom = operaciones(peso, r_fut)
print(len(op_tsmom), "operaciones")
op_tsmom.sort_values("pnl").iloc[[0, -1]]
```
```output
2576 operaciones
      entrada     salida  lado       pnl  sesiones mercado
1  2005-03-24 2005-08-24  -1.0 -0.021429       107     AGA
69 2019-04-03 2024-12-02  -1.0  0.123242      1427     AGA
```

Son 2.576 operaciones en 24 mercados, unas 107 por mercado en 21 años. Las dos extremas son del maíz ficticio AGA: la
peor resta un 2,1 % del capital (corta de marzo a agosto de 2005) y la mejor suma un 12,3 % (corta de abril de 2019 a
diciembre de 2024, 1.427 sesiones, casi seis años).
Las operaciones que siguen abiertas el 31 de diciembre de 2025 y las que empezaron antes de febrero de 2005 cuentan
solo por su parte dentro de la muestra.

Con la lista se calculan cuatro métricas. Si $p$ es la fracción de operaciones ganadoras (**win rate**), $W$ la ganancia
media de las ganadoras y $L$ la pérdida media de las perdedoras (en positivo):

$$
\text{payoff} = \frac{W}{L}, \qquad
E = p\,W - (1 - p)\,L, \qquad
PF = \frac{\sum_{\text{ganadoras}} \text{pnl}}{\left|\sum_{\text{perdedoras}} \text{pnl}\right|} = \frac{p\,W}{(1 - p)\,L}
$$

La **expectativa** $E$ es lo que se gana de media por operación. El **profit factor** $PF$ compara lo ganado con lo
perdido: mayor que 1 si y solo si $E > 0$. Igualando $E$ a cero sale el **win rate de equilibrio**, el mínimo que hace
falta para no perder con un payoff dado:

$$
p^{*} = \frac{1}{1 + W/L}
$$

Para el carry, que rebalancea cada fin de mes, la operación natural es el mes: cada mes se abre la cesta de seis
divisas (en la práctica, con forwards a un mes) y se cierra al siguiente.

```python
def metricas_op(pnl):
    gana, pierde = pnl[pnl > 0], pnl[pnl <= 0]
    p = len(gana) / len(pnl)
    W, L = gana.mean(), -pierde.mean()
    return {"operaciones": len(pnl), "win_rate": p,
            "ganancia_media_pb": 1e4 * W,
            "perdida_media_pb": 1e4 * L, "payoff": W / L,
            "expectativa_pb": 1e4 * (p * W - (1 - p) * L),
            "profit_factor": gana.sum() / -pierde.sum(),
            "win_rate_equilibrio": 1 / (1 + W / L)}

aporte_fx = (w_fx * r_fx).sum(axis=1)
mes_carry = aporte_fx.groupby(aporte_fx.index.to_period("M")).sum()
pd.DataFrame({"tsmom": metricas_op(op_tsmom["pnl"]),
              "carry_mes": metricas_op(mes_carry)}).round(3)
```
```output
                        tsmom  carry_mes
operaciones          2576.000    251.000
win_rate                0.330      0.574
ganancia_media_pb      72.069    227.469
perdida_media_pb       20.389    223.261
payoff                  3.535      1.019
expectativa_pb         10.083     35.325
profit_factor           1.738      1.371
win_rate_equilibrio     0.221      0.495
```

TSMOM acierta solo el 33,0 % de sus operaciones, pero cada ganadora vale de media 3,54 veces lo que cuesta cada
perdedora: su win rate de equilibrio es el 22,1 % y lo supera con holgura. El carry acierta el 57,4 % de los meses
con un payoff de 1,02; su equilibrio está en el 49,5 %. Las dos tienen expectativa positiva (10,1 pb por operación y
35,3 pb por mes) y profit factor por encima de 1 (1,74 y 1,37). Son dos formas opuestas de ganar.

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

El profit factor de los rendimientos diarios es el Omega con umbral cero de la lección 2.6.1: suma de días positivos
entre suma de días negativos. quantstats llama «Profit Factor» y «Payoff Ratio» a esas cifras calculadas día a día, no
operación a operación. Antes de comparar un profit factor con otro, pregunta de qué unidades está hecho: días, meses u
operaciones.

</aside>

La media esconde cómo se reparte el beneficio. Ordenando las operaciones de mejor a peor:

```python
def concentracion(pnl):
    orden = pnl.sort_values(ascending=False)
    n = len(pnl) // 10
    return {"mejor_10%": orden.iloc[:n].sum() / pnl.sum(),
            "peor_10%": orden.iloc[-n:].sum() / pnl.sum(),
            "sin_extremos": orden.iloc[n:-n].sum() / pnl.sum()}

pd.DataFrame({"tsmom": concentracion(op_tsmom["pnl"]),
              "carry_mes": concentracion(mes_carry)}).round(2)
```
```output
              tsmom  carry_mes
mejor_10%      2.12       1.45
peor_10%      -0.63      -1.60
sin_extremos  -0.49       1.15
```

*Fig. — De dónde sale el beneficio: aporte de las mejores, las centrales y las peores operaciones al beneficio total de TSMOM y del carry FX.* Dos paneles de barras horizontales en porcentaje del beneficio total. TSMOM, por operaciones: el mejor 10 %, destacado, aporta el +212 %; el 80 % central, el −49 %; el peor 10 %, el −63 %. Carry FX, por meses: el mejor 10 % aporta el +145 %; el 80 % central, el +115 %; el peor 10 %, el −160 %.

En TSMOM, el mejor 10 % de las operaciones aporta el 212 % del beneficio total: sin ellas, la estrategia pierde. El
80 % central resta un 49 % y el peor 10 %, un 63 %. Es la forma de un seguidor de tendencia: vive de unas pocas
tendencias largas, y cualquier regla que recorte las ganadoras (un objetivo de beneficio, un filtro que se salte algunas
señales) le quita justo lo que la sostiene. El carry es el espejo: el 80 % central de los meses gana (un 115 % del
total) y el peor 10 % se lleva el 160 %, los meses de crash. Una estrategia con asimetría negativa parece funcionar
todos los meses hasta el mes que no.

## Por qué el win rate engaña

Un win rate alto es fácil de fabricar: basta con poner el objetivo de beneficio cerca y el stop lejos. Para verlo hace
falta una estrategia sin ventaja, y los datos del nivel 2 la tienen: en las acciones del universo ficticio la reversión
a corto plazo **no tiene efecto plantado** (es un control). La regla compra al cierre después de una caída de más de 2
desviaciones típicas, y sale al cierre en cuanto la ganancia acumulada pasa de +1σ (objetivo) o la pérdida de −3σ
(stop), o a las 10 sesiones.

```python
aj = pd.read_parquet("universo2_ajustados.parquet")
pa = aj.pivot(index="fecha", columns="ticker", values="cierre_ajustado")
lp = np.log(pa)
sig = lp.diff().ewm(span=60, min_periods=60).std().shift(1)

def reversion(objetivo=1.0, stop=3.0, max_ses=10, caida=2.0):
    """Compra al cierre tras caer `caida` sigmas; sale al cierre
    al tocar +objetivo o -stop sigmas, o tras max_ses sesiones."""
    filas = []
    for tk in pa.columns:
        x = lp[tk].dropna()
        v, s = x.to_numpy(), sig[tk].reindex(x.index).to_numpy()
        i = 61
        while i < len(v) - max_ses:
            if v[i] - v[i - 1] < -caida * s[i]:
                for k in range(1, max_ses + 1):
                    c = v[i + k] - v[i]
                    if (c >= objetivo * s[i] or c <= -stop * s[i]
                            or k == max_ses):
                        break
                filas.append((tk, x.index[i], k, np.expm1(c)))
                i += k
            i += 1
    return pd.DataFrame(filas, columns=["ticker", "entrada",
                                        "sesiones", "pnl"])

op_rev = reversion()
op_rev.head(3)
```
```output
  ticker    entrada  sesiones       pnl
0   QXAC 2006-05-01         4  0.008333
1   QXAC 2006-06-27         4 -0.035329
2   QXAC 2006-07-05         5  0.023794
```

En un paseo aleatorio sin deriva, un objetivo a $+a$ y un stop a $-b$ se tocan antes con probabilidad $b/(a+b)$: con 1σ
y 3σ, el 75 % de las veces gana el objetivo. Pero la ganancia es tres veces menor que la pérdida, así que la
expectativa es cero: la regla redistribuye el resultado, no lo crea. Con los datos y sus costes:

```python
ch = pd.read_parquet("universo2_caracteristicas.parquet")
spr = ch.pivot(index="fecha", columns="ticker", values="horquilla_pb")
spr.index = spr.index.to_period("M")
mes_ant = op_rev["entrada"].dt.to_period("M") - 1
coste = [spr.at[m, t] if m in spr.index else np.nan
         for m, t in zip(mes_ant, op_rev["ticker"])]
op_rev["coste"] = pd.Series(coste).fillna(spr.stack().median()) / 1e4

pd.DataFrame({"bruto": metricas_op(op_rev["pnl"]),
              "neto": metricas_op(op_rev["pnl"] - op_rev["coste"])}
             ).round(3)
```
```output
                         bruto       neto
operaciones          12633.000  12633.000
win_rate                 0.634      0.633
ganancia_media_pb      321.795    315.899
perdida_media_pb       620.908    627.096
payoff                   0.518      0.504
expectativa_pb         -23.333    -29.709
profit_factor            0.897      0.871
win_rate_equilibrio      0.659      0.665
```

La regla acierta el 63,4 % de 12.633 operaciones (menos del 75 % teórico, porque muchas salen por tiempo y los
cierres saltan por encima de los umbrales) con un payoff de 0,52. Su win rate de equilibrio es el 65,9 %: se queda
2,5 puntos por debajo y pierde 23,3 pb por operación antes de costes. La horquilla de cada acción (la del mes anterior
a la entrada, de `universo2_caracteristicas.parquet`) añade 6,4 pb, hasta −29,7 pb.

¿Por qué pierde incluso sin costes, si la reversión a corto plazo no tiene efecto plantado? Las caídas de más de 2σ no
se reparten por igual en el tiempo: se concentran en los tramos de estrés del guion (las crisis con fechas fijas del
README de los datos; ocho desde 2006), en los que el mercado tiene una deriva muy negativa durante semanas.

```python
tramos = [("2008-09-02", "2009-03-09"), ("2010-04-26", "2010-07-05"),
          ("2011-07-25", "2011-10-03"), ("2015-08-17", "2016-02-11"),
          ("2018-10-01", "2018-12-25"), ("2020-02-20", "2020-03-23"),
          ("2022-01-04", "2022-10-12"), ("2025-02-20", "2025-04-08")]
en_estres = np.zeros(len(op_rev), dtype=bool)
for a, b in tramos:
    en_estres |= op_rev["entrada"].between(a, b).to_numpy()
pd.DataFrame({"fuera": metricas_op(op_rev["pnl"][~en_estres]),
              "estres": metricas_op(op_rev["pnl"][en_estres])}).round(3)
```
```output
                        fuera    estres
operaciones          9542.000  3091.000
win_rate                0.667     0.532
ganancia_media_pb     304.080   390.319
perdida_media_pb      548.349   780.430
payoff                  0.555     0.500
expectativa_pb         20.086  -157.369
profit_factor           1.110     0.569
win_rate_equilibrio     0.643     0.667
```

Fuera de los tramos de estrés, la regla acierta el 66,7 % con un payoff de 0,555 y gana 20,1 pb por operación: está
comprada unas pocas sesiones en un mercado con deriva positiva. Dentro, 3.091 operaciones (una de cada cuatro) aciertan
solo el 53,2 % y pierden 157 pb cada una: compra caídas en mitad de una crisis y toca el stop. Toda la pérdida sale de
ahí. Es la forma del carry de la sección anterior: ganancias pequeñas y frecuentes, pérdidas grandes agrupadas en las
crisis. Un win rate alto, muchas veces, es eso: vender sin saberlo un seguro contra el crash.

La figura de la cabecera pone las tres estrategias sobre el plano win rate-payoff. La curva continua es la expectativa
cero; por encima de ella se gana. TSMOM (y la nube de sus 24 mercados) está abajo a la derecha: acierta poco y gana
mucho cuando acierta. El carry, en el centro. La reversión, arriba a la izquierda: es la que más acierta y la única que
está por debajo de la curva.

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

La regla no abre operaciones en las 10 últimas sesiones de cada acción, para no salirse de los datos. Con eso evita las
quiebras justo antes del deslistado, cuya rentabilidad final es −100 %: un sesgo de supervivencia pequeño a favor de la
regla (lección 2.5.1). Un backtest honesto tendría que dejar abiertas esas operaciones y aplicarles la rentabilidad de
deslistado de `universo2_eventos.csv`.

</aside>

## Alfa y beta frente a un índice

La segunda pregunta es otra: de lo que gana una cartera, qué parte habría ganado cualquiera expuesto al mercado. La
regresión de los excesos de rentabilidad sobre los del índice la separa:

$$
r_t - r_{f,t} = \alpha + \beta\,(r_{m,t} - r_{f,t}) + \varepsilon_t
$$

$\beta$ es la exposición al índice y $\alpha$ la rentabilidad media que no explica. Para probarlas hacen falta cuatro
carteras sobre el universo de 120 acciones ficticias, con datos point-in-time (lección 1.4.4) y la rentabilidad de
deslistado de las quiebras:

- **Índice**: los miembros del índice de cada mes, equiponderados. Es el benchmark.
- **Factores**: las 20 acciones con mayor puntuación compuesta de momentum 12-1, value y baja volatilidad, las tres
  primas plantadas en los datos. El efecto existe.
- **ESG**: las 20 de mayor puntuación ESG, un placebo puro (un AR(1) independiente de todo). La verdad es cero.
- **Calidad** (ROE) y **tamaño**: los dos controles sin prima. La calidad tiene una trampa documentada en los datos:
  el ROE es E/P por P/VC, está muy correlado con el value y en un ordenamiento univariante hereda su prima con el signo
  contrario.

```python
from scipy import stats

ev = pd.read_csv("universo2_eventos.csv")
desl = ev.dropna(subset=["rentabilidad_deslistado"])
rd = pa.pct_change(fill_method=None)
for tk, r_desl in zip(desl["ticker"], desl["rentabilidad_deslistado"]):
    i = pa.index.get_loc(pa[tk].last_valid_index())
    if i + 1 < len(pa.index):          # rentabilidad de deslistado
        rd.loc[pa.index[i + 1], tk] = r_desl
mes = rd.index.to_period("M")
rm = ((1 + rd.fillna(0)).groupby(mes).prod() - 1).where(
    rd.notna().groupby(mes).any())

def piv(col):     # característica a fin de mes (point-in-time)
    t = ch.pivot(index="fecha", columns="ticker", values=col)
    return t.set_axis(rm.index, axis=0).reindex(columns=rm.columns)

miembro = piv("miembro").fillna(False).astype(bool)
fin = pa.index.to_series().groupby(mes).max()
vol252 = rd.rolling(252, min_periods=126).std().loc[fin]
vol252.index = rm.index
mom = (1 + rm).rolling(11).apply(np.prod, raw=True).shift(1) - 1
value = np.log(piv("patrimonio_neto") / piv("capitalizacion"))

def z(s):           # puntuación normal por rangos entre miembros
    s = s.where(miembro)
    q = (s.rank(axis=1) - 0.5).div(s.notna().sum(axis=1), axis=0)
    return pd.DataFrame(stats.norm.ppf(q), s.index, s.columns)

def top(puntos, n=20):          # 20 mayores, equiponderadas
    rk = puntos.where(miembro).rank(axis=1, ascending=False)
    w = (rk <= n).astype(float)
    return w.div(w.sum(axis=1), axis=0)

compuesto = (z(mom).fillna(0) + z(value).fillna(0)
             - z(vol252).fillna(0))
pesos = {"indice": miembro.astype(float).div(miembro.sum(axis=1),
                                             axis=0),
         "factores": top(compuesto),
         "esg": top(piv("puntuacion_esg")),
         "calidad": top(piv("roe")),
         "tamano": top(-np.log(piv("capitalizacion")))}
rent = pd.DataFrame({k: (w.shift(1) * rm).sum(axis=1, min_count=1)
                     for k, w in pesos.items()}).loc["2007-01":]
tl = pd.read_csv("tipo_libre2.csv", parse_dates=["fecha"],
                 index_col="fecha")["tipo_3m"]
rf = (tl / 1200).groupby(tl.index.to_period("M")).mean()
exceso = rent.sub(rf.reindex(rent.index), axis=0)
pd.DataFrame({"media": 12 * exceso.mean(),
              "vol": np.sqrt(12) * exceso.std(),
              "sharpe": np.sqrt(12) * exceso.mean() / exceso.std()}
             ).round(3)
```
```output
          media    vol  sharpe
indice    0.068  0.220   0.308
factores  0.109  0.196   0.558
esg       0.092  0.218   0.421
calidad   0.023  0.223   0.105
tamano    0.072  0.241   0.299
```

Las rentabilidades mensuales `rm` salen de los precios ajustados más la rentabilidad de deslistado de
`universo2_eventos.csv`. `universo2_caracteristicas.parquet` trae además una última fila por cada una de las diez
acciones que dejan de cotizar, sin precio, con `miembro = False` y con esa misma rentabilidad encadenada en
`rentabilidad_mes` (−100 % en las quiebras). Aquí no cambian nada: las señales y los pesos se forman solo con los
miembros, y el mes del deslistado ya está en `rm`. Quien use `rentabilidad_mes` en lugar de calcularla tiene que
conservar esas filas para el rendimiento y filtrarlas (por `cierre` no vacío) para las señales: sin ellas, una cartera
que tuviera una de las quiebras no vería su −100 %.

Para el error de $\alpha$ se usan errores de Newey-West[^1] (lección 2.1.1), robustos a la heterocedasticidad y a la
autocorrelación de los residuos, con 5 retardos (una regla práctica habitual, $4\,(T/100)^{2/9}$, da 4,8 con 228
meses):

```python
import statsmodels.api as sm

def alfa_beta(y, x, lags=5):
    X = sm.add_constant(x.rename("beta"))
    ols = sm.OLS(y, X).fit()
    hac = sm.OLS(y, X).fit(cov_type="HAC", cov_kwds={"maxlags": lags})
    return {"beta": hac.params["beta"],
            "alfa_anual": 12 * hac.params["const"],
            "t_ols": ols.tvalues["const"],
            "t_hac": hac.tvalues["const"],
            "ee_alfa_anual": 12 * hac.bse["const"]}

pd.DataFrame({k: alfa_beta(exceso[k], exceso["indice"])
              for k in ["factores", "esg", "calidad", "tamano"]}
             ).T.round(3)
```
```output
           beta  alfa_anual  t_ols  t_hac  ee_alfa_anual
factores  0.847       0.052  3.670  4.093          0.013
esg       0.960       0.027  2.152  2.006          0.013
calidad   0.986      -0.043 -3.529 -2.907          0.015
tamano    1.065      -0.000 -0.014 -0.013          0.014
```

*Fig. — Alfa anual de cuatro carteras de 20 acciones frente al índice equiponderado, 2007-2025, con errores de Newey-West: el placebo ESG pasa el listón de t = 2 por azar.* Cuatro intervalos horizontales de ± 2 errores típicos. Factores, con prima plantada: alfa del 5,2 % anual, t de 4,1. ESG, un placebo sin prima, destacado: 2,7 %, t de 2,0, con el intervalo rozando el cero. Calidad, sin prima propia pero confundida con el value: −4,3 %, t de −2,9. Tamaño, control: 0,0 %, t de 0,0.

- **Factores**: beta 0,85 y alfa del 5,2 % anual con t = 4,1. El método recupera un efecto que existe.
- **ESG**: alfa del 2,7 % con t = 2,0. Un placebo pasa el listón clásico de dos errores típicos. No es un fallo del
  método: con un umbral de t = 2, más o menos una de cada veinte pruebas sin efecto lo pasa por azar. Por eso un t de 2
  en la primera característica que se prueba no basta, y en la vigésima no significa nada (lección 2.1.8).
- **Calidad**: alfa del −4,3 % con t = −2,9. «Significativo» y falso: no hay prima de calidad en los datos; la
  cartera de ROE alto tiene P/VC alto (value bajo) y paga la prima del value con el signo contrario. Sin controlar por
  value, la regresión atribuye a la calidad lo que es del value.
- **Tamaño**: alfa del 0,0 % y t = 0,0. El control se comporta como debe.

Los errores de Newey-West cambian poco los t en estas carteras (de 3,67 a 4,09 en la de factores): sus residuos
mensuales casi no tienen autocorrelación. En carteras con solapes (rendimientos de 12 meses calculados cada mes) o con
activos poco líquidos, la diferencia es grande.

```mermaid
flowchart TB
  R[Rentabilidad] --> M[Beta x mercado]
  R --> A[Alfa]
  R --> E[Residuo]
  A --> AP[Appraisal ratio]
  E --> AP
  R --> ACT[Activa vs índice]
  ACT --> IR[Information ratio]
```

## Information ratio y tracking error

Un gestor que compite con un índice se mide por su **rentabilidad activa**, la diferencia con el índice. Su desviación
típica es el **tracking error** y el cociente, el **information ratio**:

$$
IR = \frac{\overline{r - r_m}}{\sigma(r - r_m)}\,\sqrt{12}
$$

El IR no ajusta por beta: una cartera con beta 0,85 pierde frente al índice en los meses alcistas por su menor
exposición, y eso entra en su tracking error. El **appraisal ratio** divide el alfa de la regresión por la desviación
típica del residuo: mide la calidad de la parte no explicada por el mercado.

```python
def ratio_informacion(r, ref, n=12):
    activa = r - ref
    te = activa.std() * np.sqrt(n)
    return {"activa_anual": activa.mean() * n, "tracking_error": te,
            "ir": activa.mean() * n / te}

def appraisal(y, x, n=12):
    fit = sm.OLS(y, sm.add_constant(x)).fit()
    return n * fit.params.iloc[0] / (fit.resid.std() * np.sqrt(n))

pd.DataFrame({k: {**ratio_informacion(rent[k], rent["indice"]),
                  "appraisal": appraisal(exceso[k], exceso["indice"])}
              for k in ["factores", "esg"]}).T.round(3)
```
```output
          activa_anual  tracking_error     ir  appraisal
factores         0.042           0.070  0.595      0.847
esg              0.024           0.054  0.440      0.497
```

La cartera de factores bate al índice en un 4,2 % anual con un tracking error del 7,0 %: IR de 0,60. Su appraisal
ratio es mayor, 0,85, porque al quitar la beta de 0,85 desaparece del denominador el ruido que pone el mercado. El
placebo ESG tiene un IR de 0,44: con 19 años, también es compatible con cero. La regla para leerlos es la misma que
para el Sharpe en la lección 2.6.1: un IR de 0,5 con 19 años tiene un error típico de unos 0,23.

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

`quantstats.stats.information_ratio` devuelve la media diaria de la rentabilidad activa dividida por su desviación
típica, **sin anualizar**: un IR anual de 0,6 aparece como 0,04 en datos diarios. La lección 2.6.3 lo compara con este.

</aside>

## Turnover y capacidad

Las métricas anteriores son brutas de costes. Para pasarlas a netas hay que saber cuánto se opera. El **turnover** de
un rebalanceo es la mitad de la suma de los cambios absolutos de los pesos (cada compra tiene una venta enfrente) y el
anual, su suma en un año:

$$
TO = \frac{1}{2}\sum_i \left| w_{i,t} - w_{i,t-1} \right|
$$

```python
def turnover_anual(w, n=12):
    cambio = w.fillna(0).diff().abs().sum(axis=1) / 2
    return cambio.loc["2007-01":].mean() * n

pd.Series({k: turnover_anual(w) for k, w in pesos.items()}).round(2)
```
```output
indice      0.01
factores    2.28
esg         2.65
calidad     0.40
tamano      0.64
dtype: float64
```

El índice casi no rota (un 1 % al año: solo cambian los miembros). La cartera de factores rota el 228 %: cada año
renueva su contenido más de dos veces, porque el momentum y la volatilidad cambian cada mes. El placebo ESG rota más
aún (265 %), porque su puntuación es casi ruido. La calidad (40 %) y el tamaño (64 %) son características lentas. El
cálculo usa los pesos objetivo de cada fin de mes y desprecia la deriva de los pesos entre rebalanceos: es una
aproximación por abajo.

La **capacidad** es el capital a partir del cual los costes se comen la ventaja (lección 2.2.3). Depende de cuánto se
opera, del tamaño de cada orden frente al volumen de la acción y de cómo crece el coste con ese tamaño. El modelo de
la celda, deliberadamente simple, cobra por cada unidad operada media horquilla más un impacto de raíz cuadrada:

$$
c_i = \frac{s_i}{2} + \sigma_i \sqrt{\frac{\Delta w_i \cdot K}{ADV_i}}
$$

con $s_i$ la horquilla, $\sigma_i$ la volatilidad diaria, $K$ el capital gestionado y $ADV_i$ el volumen medio diario
en dólares de la acción (`volumen_medio_21d` por el cierre). El coeficiente del impacto es 1, un supuesto: la lección
2.4.7 explica cómo se calibra y por qué el impacto real depende de cómo se reparte la orden en el tiempo.

```python
adv = piv("volumen_medio_21d") * piv("cierre")    # USD por sesión
spr_m = piv("horquilla_pb") / 1e4
dw = pesos["factores"].fillna(0).diff().abs().loc["2007-01":]
adv, spr_m, sd = (t.reindex_like(dw) for t in (adv, spr_m, vol252))

def capacidad(capital):
    part = dw * capital / adv              # fracción del ADV
    coste = dw * (spr_m / 2 + sd * np.sqrt(part))
    operadas = part.where(dw > 0).stack().dropna()
    return {"coste_anual": 12 * coste.sum(axis=1).mean(),
            "participacion_p95": operadas.quantile(0.95)}

pd.DataFrame({f"{c:.0e}": capacidad(c)
              for c in [1e6, 1e7, 1e8, 3e8, 1e9, 3e9]}).T.round(4)
```
```output
       coste_anual  participacion_p95
1e+06       0.0030             0.0019
1e+07       0.0068             0.0190
1e+08       0.0191             0.1902
3e+08       0.0322             0.5707
1e+09       0.0577             1.9024
3e+09       0.0992             5.7072
```

*Fig. — Capacidad de la cartera de factores: alfa anual neto de horquilla e impacto según el capital gestionado (modelo de raíz cuadrada supuesto en la lección).* Curva descendente con el capital gestionado en escala logarítmica, de 1 a 10.000 millones de USD. Con 1 millón, el alfa neto es del 4,9 %, cerca del bruto del 5,2 %; con 100 millones, del 3,3 %; se anula, en el punto destacado, hacia 808 millones, y con 10.000 millones es del −12,8 %. Una línea vertical en 53 millones marca el capital a partir del cual el 5 % de las órdenes supera el 10 % del volumen medio diario de su acción.

Con un millón de dólares, el coste es casi solo la horquilla: un 0,30 % al año frente a un alfa bruto del 5,2 %. Con
100 millones, el 1,91 %, y el percentil 95 de las órdenes es ya el 19 % del volumen diario de su acción. Con 1.000
millones, el 5,8 %, más que todo el alfa, y la orden del percentil 95 casi dobla el volumen diario: no se podría
ejecutar en una sesión. Con este modelo, la cartera pierde la mitad del alfa hacia 190 millones y todo hacia 808
millones. Un límite de ejecución práctico (que el 95 % de las órdenes no pase del 10 % del ADV) la deja en unos 53
millones.

```mermaid
flowchart TB
  K[Capital] --> O[Orden por acción]
  TO[Turnover] --> O
  O --> PA[Participación ADV]
  PA --> IM[Impacto]
  S[Horquilla] --> CO[Coste]
  IM --> CO
  CO --> AN[Alfa neto]
```

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

La capacidad estimada hereda todos los errores del alfa: con ± 2 errores típicos, el alfa de la cartera de factores
está entre el 2,7 % y el 7,7 %, y el capital de equilibrio cambia en la misma proporción. Además el universo es
pequeño (100 miembros, ADV mediano de unos 146 millones de USD por acción): en un universo real más grande y líquido,
las cifras serían otras. Lo que se traslada es el método, no los 808 millones.

</aside>

## Errores típicos al leer estas métricas

- **Juzgar por el win rate.** Solo dice algo junto al payoff: compáralo siempre con $1/(1 + \text{payoff})$.
- **Mezclar unidades.** Un profit factor de días, uno de meses y uno de operaciones son tres cifras distintas.
- **Olvidar las operaciones abiertas y los deslistados.** Las que siguen abiertas al final de la muestra y las de
  acciones que dejan de cotizar cambian la expectativa.
- **Llamar alfa a una beta.** Una cartera con beta 1,2 bate al índice en un mercado alcista sin ningún alfa. Regresión
  con el benchmark adecuado y errores robustos.
- **Creer el primer t de 2.** El placebo ESG lo consigue. Cuenta cuántas características has probado.
- **Ignorar la rotación.** Un alfa bruto del 5 % con un turnover del 228 % es otra cosa con 500 millones que con uno.

## Ejercicios

1. Agrupa las operaciones de TSMOM por sector (columna `sector` de `futuros2_especificaciones.csv`) y calcula win rate,
   payoff y expectativa de cada uno. ¿Hay algún sector por debajo del equilibrio?

**Solución.**

```python
esp = pd.read_csv("futuros2_especificaciones.csv",
                  index_col="mercado")["sector"]
por_sector = op_tsmom.groupby(op_tsmom["mercado"].map(esp))["pnl"]
pd.DataFrame({s: metricas_op(g) for s, g in por_sector}).T[
    ["operaciones", "win_rate", "payoff", "expectativa_pb",
     "win_rate_equilibrio"]].round(3)
```
```output
                operaciones  win_rate  payoff  expectativa_pb  win_rate_equilibrio
agricolas             349.0     0.344   5.180          22.025                0.162
bonos                 488.0     0.357   4.677          19.575                0.176
divisas               460.0     0.328   2.312           1.537                0.302
energia               421.0     0.280   2.532          -0.183                0.283
metales               414.0     0.343   2.635           5.262                0.275
renta_variable        444.0     0.324   3.635          13.350                0.216
```

Cinco de los seis sectores están por encima de su win rate de equilibrio, con perfiles parecidos: aciertan entre el
28,0 % y el 35,7 %, y lo que cambia es el payoff. Los agrícolas tienen el payoff más alto (5,18) y la mayor
expectativa (22,0 pb); los bonos, el win rate más alto (35,7 %) y un payoff de 4,68. La energía queda justo por
debajo del equilibrio (28,0 % frente a 28,3 %, expectativa de −0,18 pb) y las divisas, justo por encima (1,5 pb). Sin
embargo, los 24 mercados tienen plantado el mismo término de momentum (0,15 por unidad de volatilidad, README de los
datos): las diferencias entre sectores son, sobre todo, ruido. Con 350 a 490 operaciones por sector no se distinguen;
para decidir qué sectores operar haría falta su error típico, no solo la cifra, y aquí descartar la energía sería
descartar un efecto que existe.

2. Repite la reversión con objetivo y stop simétricos (2σ y 2σ). ¿Qué pasa con el win rate, el payoff y la
   expectativa?

**Solución.**

```python
op_sim = reversion(objetivo=2.0, stop=2.0)
pd.Series(metricas_op(op_sim["pnl"])).round(3)
```
```output
operaciones            12522.000
win_rate                   0.495
ganancia_media_pb        459.519
perdida_media_pb         484.926
payoff                     0.948
expectativa_pb           -17.832
profit_factor              0.927
win_rate_equilibrio        0.513
dtype: float64
```

Con objetivo y stop a la misma distancia, el win rate baja al 49,5 % y el payoff sube a 0,95: la regla ya no
«acierta mucho». La expectativa sigue negativa (−17,8 pb por operación, equilibrio en el 51,3 %), por la misma razón
que antes: las compras en plena crisis. Mover los umbrales cambia la forma de los resultados, no su media; solo una
ventaja real (o evitar los tramos de estrés, si se pudiera saber de antemano) cambia la expectativa.

3. Estima la beta de TSMOM frente al futuro del índice IDA con rendimientos mensuales y errores de Newey-West. ¿Qué
   papel tendría TSMOM en una cartera que ya tiene el índice?

**Solución.**

```python
tsmom_d = (peso * r_fut).sum(axis=1)
m = pd.DataFrame({"tsmom": tsmom_d, "ida": r_fut["IDA"]})
m = (1 + m).resample("ME").prod() - 1
ab = alfa_beta(m["tsmom"], m["ida"])
print(f"beta {ab['beta']:.3f} · alfa {ab['alfa_anual']:.2%} "
      f"(t HAC {ab['t_hac']:.2f})")
print(f"correlación mensual: {m.corr().iloc[0, 1]:.3f}")
```
```output
beta 0.089 · alfa 11.38% (t HAC 4.36)
correlación mensual: 0.162
```

La beta de TSMOM frente al índice es 0,09 y la correlación mensual, 0,16: casi no depende del mercado. Su alfa frente
al índice (11,38 % anual, t de 4,36) es prácticamente toda su rentabilidad. En una cartera con el índice, TSMOM
actuaría como diversificador: añade rentabilidad con poca correlación, y en estos datos gana en las crisis largas
(2008, +26,9 %; 2011, +8,1 %; 2022, +8,4 %), pero no en la de 2020 (−1,7 %), una caída de dos meses demasiado rápida
para una señal de 12 meses. Es un resultado de datos sintéticos con el efecto plantado; Moskowitz, Ooi y Pedersen
(2012) estudian la relación real del momentum de series temporales con los mercados alcistas y bajistas.

[^1]: Whitney K. Newey y Kenneth D. West, «A Simple, Positive Semi-Definite, Heteroskedasticity and Autocorrelation
    Consistent Covariance Matrix», Econometrica 55(3), 703-708 (1987). En statsmodels, `fit(cov_type="HAC",
    cov_kwds={"maxlags": 5})`.

## Autoevaluación

1. Una estrategia acierta el 63 % de las operaciones con un payoff de 0,52. ¿Gana dinero?

   - A. Sí: acierta casi dos de cada tres veces
   - B. No: con un payoff de 0,518 el win rate de equilibrio es 1 / (1 + 0,518) = 65,9 %, y un 63,4 % se queda por debajo
   - C. Depende solo del número de operaciones
   - D. Sí, si el profit factor es mayor que el win rate

   *Respuesta B: No: con un payoff de 0,518 el win rate de equilibrio es 1 / (1 + 0,518) = 65,9 %, y un 63,4 % se queda por debajo.* Es la regla de reversión de la lección, aplicada a acciones sin efecto de reversión: objetivo a +1σ y stop a −3σ. Acierta mucho porque el objetivo está cerca, y pierde cuando toca el stop, que está tres veces más lejos. Su expectativa es de −23 pb por operación antes de costes, y toda la pérdida sale de las caídas compradas en plena crisis.

2. En TSMOM, el mejor 10 % de las operaciones aporta el 212 % del beneficio total. ¿Qué significa?

   - A. Que hay un error: un porcentaje no puede pasar de 100
   - B. Que el 90 % restante pierde en conjunto (−112 % del total): la estrategia vive de unas pocas tendencias largas y no se puede saltar ninguna
   - C. Que la estrategia tiene asimetría negativa
   - D. Que conviene quitar las operaciones grandes

   *Respuesta B: Que el 90 % restante pierde en conjunto (−112 % del total): la estrategia vive de unas pocas tendencias largas y no se puede saltar ninguna.* Es la forma típica de un seguidor de tendencia: muchas pérdidas pequeñas y pocas ganancias grandes. Cualquier filtro que elimine algunas de las grandes (por ejemplo, un stop de beneficio) destruye la estrategia, y un periodo sin tendencias produce rachas largas de pérdidas.

3. La cartera de 20 acciones con más puntuación ESG (un placebo sin ninguna prima en los datos) tiene un alfa del 2,7 % anual con t = 2,0. ¿Qué concluyes?

   - A. Que el ESG tiene una prima del 2,7 %
   - B. Que es un falso positivo: con un umbral de t = 2, aproximadamente una de cada veinte carteras sin efecto lo pasa por azar, y aquí sabemos que la verdad es cero
   - C. Que los errores de Newey-West están mal calculados
   - D. Que el índice no es un buen benchmark

   *Respuesta B: Que es un falso positivo: con un umbral de t = 2, aproximadamente una de cada veinte carteras sin efecto lo pasa por azar, y aquí sabemos que la verdad es cero.* Los datos sintéticos permiten saber la respuesta: la puntuación ESG es un AR(1) independiente de todo. Un t de 2 en una sola prueba ya es frágil; si además se han probado varias características, hay que corregir por pruebas múltiples (lección 2.1.8).

4. ¿Qué diferencia hay entre el information ratio y el appraisal ratio?

   - A. Ninguna
   - B. El IR divide la rentabilidad activa (cartera menos índice) por su volatilidad, sin ajustar por beta; el appraisal ratio divide el alfa de la regresión por la volatilidad del residuo, después de quitar la parte explicada por el mercado
   - C. El IR solo vale para fondos indexados
   - D. El appraisal ratio usa rendimientos logarítmicos

   *Respuesta B: El IR divide la rentabilidad activa (cartera menos índice) por su volatilidad, sin ajustar por beta; el appraisal ratio divide el alfa de la regresión por la volatilidad del residuo, después de quitar la parte explicada por el mercado.* Una cartera con beta 0,85 tiene una rentabilidad activa contaminada por el mercado: en los años alcistas pierde frente al índice por su menor beta. En la cartera de factores, IR 0,60 y appraisal 0,85.

5. La cartera de factores rota un 228 % al año. Con 1 millón de USD el coste anual estimado es del 0,30 %; con 1.000 millones, del 5,8 %. ¿Por qué crece tanto?

   - A. Porque las comisiones son proporcionales
   - B. Porque la horquilla se paga igual con cualquier capital, pero el impacto crece con la participación en el volumen de cada acción: con más capital, cada orden es una fracción mayor del ADV y mueve más el precio
   - C. Porque con más capital hay que operar más veces
   - D. Porque el tipo libre sube

   *Respuesta B: Porque la horquilla se paga igual con cualquier capital, pero el impacto crece con la participación en el volumen de cada acción: con más capital, cada orden es una fracción mayor del ADV y mueve más el precio.* La horquilla es un coste fijo por unidad operada; el impacto, con el modelo de raíz cuadrada, crece con la raíz de la participación. Con 1.000 millones, el percentil 95 de las órdenes es casi el doble del volumen medio diario de su acción: imposible de ejecutar en un día.

## Ideas clave

- Expectativa = p·W − (1 − p)·L. El win rate de equilibrio es 1 / (1 + payoff): con un payoff de 3, basta acertar el 25 %; con 0,5, hace falta el 67 %.
- El win rate solo engaña: TSMOM acierta el 33 % y gana; una reversión sin ventaja acierta el 63 % y pierde 23 pb por operación, todo en las crisis: un win rate alto suele esconder un riesgo de crash.
- La forma importa: en TSMOM el mejor 10 % de las operaciones aporta el 212 % del beneficio; en el carry, el peor 10 % de los meses se lleva el 160 %.
- Alpha y beta se estiman con errores robustos (Newey-West). En los datos, la cartera de factores plantados da t = 4,1; el placebo ESG, t = 2,0 por azar; la calidad, −2,9 por confusión con el value.
- Information ratio = rentabilidad activa / tracking error; appraisal ratio = alfa / volatilidad residual. Informa de los dos y del benchmark.
- La capacidad sale de la rotación, la horquilla y la participación en el volumen. La cartera de factores pierde la mitad del alfa hacia 190 millones y todo hacia 800 millones, con los supuestos de la lección.

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

- [statsmodels: errores robustos (HAC) en la regresión lineal](https://www.statsmodels.org/stable/generated/statsmodels.regression.linear_model.RegressionResults.get_robustcov_results.html) · statsmodels.org

### Libros

- [Whitney K. Newey y Kenneth D. West, «A Simple, Positive Semi-Definite, Heteroskedasticity and Autocorrelation Consistent Covariance Matrix», Econometrica 55(3), 703-708 (1987)](https://doi.org/10.2307/1913610) · doi.org
- [Tobias J. Moskowitz, Yao Hua Ooi y Lasse Heje Pedersen, «Time series momentum», Journal of Financial Economics 104(2), 228-250 (2012)](https://doi.org/10.1016/j.jfineco.2011.11.003) · doi.org

### Datos

- [Datos del nivel 2: características mensuales del universo (Parquet)](https://pythonparatrading.com/curso/datos/nivel-2/universo2_caracteristicas.parquet) · Descarga · universo2_caracteristicas.parquet

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

Lección siguiente: [2.6.3 · Informes con QuantStats o pyfolio-reloaded y tearsheets propios](https://pythonparatrading.com/curso-python-trading/nivel-2/informes-quantstats-y-tearsheets/)
