---
title: "2.6.1 Métricas de rentabilidad y riesgo"
description: "CAGR, volatilidad, Sharpe con su error típico, Sortino, Calmar, Omega, drawdown, duración y Ulcer Index, desde cero y frente a empyrical-reloaded."
url: "https://pythonparatrading.com/curso-python-trading/nivel-2/metricas-de-rentabilidad-y-riesgo/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-2/metricas-de-rentabilidad-y-riesgo.png"
---

# 2.6.1 · CAGR, Sharpe, Sortino, Calmar, Omega, drawdown y Ulcer Index

> Una curva de capital se resume en unas pocas cifras, y cada una esconde una convención: qué cuenta como un año, qué tipo libre se resta, qué parte de la distribución se mira. Esta lección las implementa desde cero sobre tres estrategias con formas de rentabilidad muy distintas y comprueba que coinciden con empyrical-reloaded.

- 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: [0.1.2 · Expectativas realistas: costes, tiempo y riesgo de pérdida](https://pythonparatrading.com/curso-python-trading/nivel-0/expectativas-realistas-costes-tiempo-riesgo/), [0.7.1 · Rentabilidades simples y logarítmicas, interés compuesto y estadística descriptiva](https://pythonparatrading.com/curso-python-trading/nivel-0/rentabilidades-y-estadistica-descriptiva/), [1.6.1 · Rentabilidades, volatilidad, correlaciones móviles y drawdown](https://pythonparatrading.com/curso-python-trading/nivel-1/volatilidad-correlaciones-y-drawdown/)

## Al terminar esta lección

- Calcular CAGR, volatilidad, Sharpe, Sortino, Calmar y Omega desde los rendimientos diarios, con la convención de anualización correcta para cada mercado (252 sesiones, 261 días laborables o fechas de calendario).
- Restar el tipo libre cuando la serie es una rentabilidad total y no restarlo cuando ya es un exceso, como en futuros o en divisas financiadas.
- Estimar el error típico de un Sharpe con la fórmula de Lo (2002), su versión con asimetría y curtosis, y la corrección por autocorrelación al anualizar.
- Medir el drawdown en profundidad y en duración y resumirlo con el Ulcer Index y el ratio de Martin.
- Explicar por qué un carry con asimetría negativa y un índice pueden tener un Sharpe parecido y riesgos muy distintos.

## Tres formas de ganar dinero

Un backtest devuelve una serie de rendimientos, y casi todo lo que se dice de una estrategia sale de resumirla en unas
pocas cifras. Cada cifra mira una cosa distinta de la serie, y para ver qué mira cada una conviene medirlas sobre
estrategias que ganan dinero de formas distintas. Esta lección construye tres sobre los datos del nivel 2:

- **Índice IDA.** Largo en el futuro continuo de un índice de renta variable ficticio, comprado y mantenido. Es la
  referencia: gana la prima de la renta variable y sufre todas sus crisis.
- **TSMOM en futuros.** Momentum de series temporales en los 24 mercados de `futuros2_continuos.parquet`: largo si el
  mercado ha subido en las últimas 252 sesiones, corto si ha bajado, con cada posición dimensionada para un 40 % de
  volatilidad anual entre 24, como en Moskowitz, Ooi y Pedersen (2012)[^1]. Su forma típica: muchas pérdidas pequeñas
  cuando no hay tendencia y ganancias grandes cuando la hay.
- **Carry de divisas.** Cada fin de mes, largo en las tres divisas de tipo de interés más alto y corto en las tres de
  tipo más bajo de `fx2_carry.parquet`. Cobra el diferencial de tipos casi todos los días y lo devuelve de golpe en los
  crashes.

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

Los datos son sintéticos y los dos efectos están plantados con parámetros conocidos (README de los datos del nivel 2):
el rendimiento esperado de cada futuro lleva un término de momentum con un Sharpe de unos 0,15 por mercado (los
índices, además, la prima de la renta variable con las crisis del guion), y cada divisa gana en promedio el 70 % de su
diferencial de tipos y lo pierde en cinco crashes con fechas fijas. Que estas estrategias ganen aquí no dice nada de
los mercados reales. Para la evidencia real, la literatura: Moskowitz, Ooi y Pedersen (2012) para el momentum de
series temporales y Brunnermeier, Nagel y Pedersen (2008)[^2] para el riesgo de crash del carry.

</aside>

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

# 1) Futuros: largo en IDA y momentum de series temporales (TSMOM)
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)
tsmom = (peso * r_fut).sum(axis=1, min_count=1)

# 2) Carry de divisas: largo en las 3 de tipo más alto, corto en 3
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    # spot + diferencial devengado
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)
carry = (w_fx * r_fx).sum(axis=1, min_count=1)

inicio = "2005-02-01"
rets = {"indice": r_fut["IDA"].loc[inicio:],
        "tsmom": tsmom.loc[inicio:],
        "carry": carry.loc[inicio:]}

def describe(r):
    anos = (r.index[-1] - r.index[0]).days / 365.25
    m = (1 + r).resample("ME").prod() - 1
    return {"sesiones": len(r), "por_ano": len(r) / anos,
            "asim_diaria": r.skew(), "asim_mensual": m.skew(),
            "curtosis_mensual": m.kurt()}

(pd.DataFrame({k: describe(r) for k, r in rets.items()}).T
 .astype({"sesiones": int}).round(2))
```
```output
        sesiones  por_ano  asim_diaria  asim_mensual  curtosis_mensual
indice      5263   251.68         0.12         -2.15             15.90
tsmom       5263   251.68         0.02          0.16             -0.07
carry       5457   260.95        -0.71         -1.38              7.39
```

Las tres empiezan el 1 de febrero de 2005, el primer día con pesos del carry: comparar métricas de periodos distintos
es el primer error de cualquier tabla de estrategias, porque cada periodo trae sus propias crisis. Se guardan en un
diccionario y no en un DataFrame porque no comparten calendario: los futuros cotizan en las sesiones de la NYSE (251,7
al año) y las divisas en días laborables (261 al año, sin festivos). En un DataFrame, los festivos de la NYSE serían
huecos del carry.

La tabla ya enseña la diferencia de forma. La asimetría mensual del carry es −1,38 y su curtosis de exceso, 7,39: una
cola izquierda larga. La del índice es aún más extrema (−2,15 y 15,90), pero por un solo mes: marzo de 2020, con un
−48,3 % (la crisis del guion de febrero y marzo, que el índice comparte con las acciones del nivel). Sin ese mes, la
del índice queda en −0,46, la misma que la del carry sin su peor mes (febrero de 2009). TSMOM es la única con
asimetría mensual positiva (0,16) y colas finas (curtosis de exceso −0,07). La figura de la cabecera lo muestra en el
capital acumulado: el índice cae en 2008 y en 2020 y se recupera en unos tres años cada vez; el carry sube con
suavidad hasta 2008, cae de golpe y tarda una década en recuperarse.

```mermaid
flowchart TB
  R[Rendimientos] --> A[Crecimiento]
  R --> V[Dispersión]
  R --> T[Trayectoria]
  A --> CAGR[CAGR]
  V --> S[Sharpe, Sortino]
  V --> O[Omega]
  T --> D[DD, Calmar, Ulcer]
```

Las métricas de esta lección se reparten en tres familias. Las de **crecimiento** dicen cuánto ha crecido el capital.
Las de **dispersión** comparan la media con la variabilidad de los rendimientos y no dependen de su orden: barajar los
días no cambia un Sharpe. Las de **trayectoria** dependen del orden: el mismo conjunto de días, ordenado con las
pérdidas juntas, da un drawdown mucho peor. Para ver el riesgo de una estrategia hacen falta las tres.

## Rentabilidad: el CAGR y qué es un año

La tasa de crecimiento anual compuesta es el crecimiento constante que lleva del capital inicial al final:

$$
\text{CAGR} = \left(\frac{V_T}{V_0}\right)^{365{,}25 / D} - 1
$$

con $D$ los días de calendario entre la primera y la última fecha. La lección 0.7.1 ya lo calculó así. Las librerías
lo hacen de otra forma: cuentan los años como sesiones entre 252.

```python
def cagr_fechas(r):
    anos = (r.index[-1] - r.index[0]).days / 365.25
    return (1 + r).prod() ** (1 / anos) - 1

def cagr_sesiones(r, n=252):
    return (1 + r).prod() ** (n / len(r)) - 1

pd.DataFrame({k: {"por_fechas": cagr_fechas(r),
                  "por_sesiones_252": cagr_sesiones(r),
                  "media_x252": r.mean() * 252}
              for k, r in rets.items()}).T.round(4)
```
```output
        por_fechas  por_sesiones_252  media_x252
indice      0.0971            0.0972      0.1134
tsmom       0.1239            0.1241      0.1244
carry       0.0389            0.0375      0.0409
```

En los futuros las dos cuentas coinciden, porque la NYSE tiene 251,7 sesiones al año. En el carry no: 5.457 días
laborables entre 252 son 21,7 «años», cuando el calendario solo tiene 20,9, y el mismo crecimiento repartido entre más
años da un CAGR del 3,75 % en lugar del 3,89 %. Catorce puntos básicos al año, por una convención. En cripto, que
cotiza 365 días, el error sería mucho mayor.

La última columna es la media diaria por 252, la «rentabilidad anual» aritmética. Siempre es mayor que el CAGR, y la
diferencia crece con la volatilidad: es el término $\sigma^2/2$ de la lección 0.7.1. En el índice, con un 20 % de
volatilidad, la media por 252 (11,34 %) supera al CAGR (9,71 %) en 1,6 puntos. Cuando un informe dice «rentabilidad
anual», hay que preguntar cuál de las dos.

```mermaid
flowchart TB
  Q{¿Qué se anualiza?} -- crecimiento --> F[Años por fechas]
  Q -- media o vol --> N{¿Mercado?}
  N -- bolsa EE. UU. --> B[252 sesiones]
  N -- divisas --> D[261 días]
  N -- cripto --> C[365 días]
```

## Volatilidad y ratio de Sharpe

La volatilidad anual es la desviación típica diaria por la raíz de las sesiones del año. El **ratio de Sharpe** divide
la rentabilidad media en exceso del tipo libre entre su desviación típica[^3]:

$$
SR = \frac{\overline{r - r_f}}{\sigma(r - r_f)}\,\sqrt{n}
$$

con $n$ las observaciones por año. Las dos se anualizan con $\sqrt{n}$ suponiendo rendimientos independientes; más
abajo se ve qué pasa cuando no lo son.

```python
def vol_anual(r, n=252):
    return r.std() * np.sqrt(n)

def sharpe(r, rf=0.0, n=252):
    exceso = r - rf
    return exceso.mean() / exceso.std() * np.sqrt(n)

def sesiones_por_ano(r):
    return len(r) / ((r.index[-1] - r.index[0]).days / 365.25)

pd.DataFrame({k: {"vol": vol_anual(r), "sharpe_252": sharpe(r),
                  "sharpe_real": sharpe(r, n=sesiones_por_ano(r))}
              for k, r in rets.items()}).T.round(3)
```
```output
          vol  sharpe_252  sharpe_real
indice  0.203       0.559        0.559
tsmom   0.121       1.026        1.025
carry   0.091       0.451        0.459
```

Con 252 o con las sesiones reales, los Sharpe de los futuros apenas cambian (de 1,026 a 1,025 en TSMOM). El del carry
pasa de 0,451 a 0,459: anualizar
por 252 una serie de 261 días por año la infravalora un 1,8 % ($\sqrt{261/252}$). Es poco, pero es un sesgo
sistemático contra las divisas cuando se comparan con acciones en la misma tabla.

El tipo libre necesita más cuidado. Un futuro no requiere desembolsar el nocional: se deposita un margen y el resto del
capital puede estar en letras cobrando el tipo libre. Por eso la rentabilidad de un futuro ya es un **exceso** sobre el
tipo libre, y lo mismo pasa con una posición en divisas financiada en dólares, que cobra el diferencial de tipos. Sobre
esas series, el Sharpe se calcula sin restar nada. Una cartera de acciones, en cambio, da una rentabilidad **total**, y
hay que restar el tipo libre de cada día. El error típico es mezclar las dos:

```python
tl = pd.read_csv("tipo_libre2.csv", parse_dates=["fecha"],
                 index_col="fecha")["tipo_3m"]
rf = (tl / 100 / 252).reindex(rets["tsmom"].index).ffill()
total = rets["tsmom"] + rf      # el colateral cobra el tipo libre

print(f"Tipo libre medio: {rf.mean() * 252:.2%} anual")
print(f"Sharpe del total sin restar rf: {sharpe(total):.3f}")
print(f"Sharpe del total restando rf:   {sharpe(total, rf):.3f}")
print(f"Sharpe del exceso (la serie):   {sharpe(rets['tsmom']):.3f}")
```
```output
Tipo libre medio: 1.79% anual
Sharpe del total sin restar rf: 1.173
Sharpe del total restando rf:   1.026
Sharpe del exceso (la serie):   1.026
```

Si a TSMOM se le suma el interés del colateral (el tipo libre sintético de `tipo_libre2.csv`, un 1,79 % anual de
media) y se calcula el Sharpe sin restarlo, sale 1,173 en lugar de 1,026. Restando el tipo de cada día se vuelve
exactamente a la serie en exceso. El error crece con el nivel de tipos: con letras al 5 %, una estrategia que no hace
nada tiene un «Sharpe» enorme, porque su volatilidad es casi cero.

## Cuánto se puede fiar uno de un Sharpe

Un Sharpe medido es una estimación, y tiene error típico. Lo (2002)[^4] lo deriva para rendimientos independientes y
normales:

$$
SE(\widehat{SR}) \approx \sqrt{\frac{1 + \tfrac{1}{2}\,SR^2}{T}}
$$

con $SR$ y $T$ en la misma frecuencia (por ejemplo, Sharpe mensual y número de meses). Como el Sharpe mensual es
pequeño, el numerador es casi 1 y el error del Sharpe anual queda en torno a $1/\sqrt{A}$, con $A$ los años de
datos: con 21 años, 0,22. La lección 0.1.2 ya usó esa aproximación para calcular cuántos años hacen falta para distinguir un Sharpe del
azar.

Dos correcciones importan aquí. La primera, la forma de la distribución: con asimetría $\gamma_3$ y curtosis
$\gamma_4$ (no de exceso), el error es[^5]

$$
SE(\widehat{SR}) \approx \sqrt{\frac{1 - \gamma_3\,SR + \tfrac{\gamma_4 - 1}{4}\,SR^2}{T - 1}}
$$

y una asimetría negativa lo aumenta. La segunda, la autocorrelación: multiplicar un Sharpe mensual por $\sqrt{12}$
supone meses independientes. Lo (2002) da el factor correcto,

$$
SR_{\text{anual}} = \eta(q)\,SR_{\text{mensual}}, \qquad
\eta(q) = \frac{q}{\sqrt{q + 2\sum_{k=1}^{q-1}(q-k)\,\rho_k}}
$$

con $q = 12$ y $\rho_k$ la autocorrelación de los rendimientos mensuales en el retardo $k$. Con autocorrelación positiva,
$\eta(12)$ es menor que $\sqrt{12} \approx 3{,}464$ y el Sharpe anual baja.

```python
from scipy import stats

def error_sharpe(r):
    m = (1 + r).resample("ME").prod() - 1
    T, sr = len(m), m.mean() / m.std()
    g3, g4 = stats.skew(m), stats.kurtosis(m, fisher=False)
    iid = np.sqrt((1 + sr**2 / 2) / T)
    no_normal = np.sqrt((1 - g3 * sr + (g4 - 1) / 4 * sr**2)
                        / (T - 1))
    rho = [m.autocorr(k) for k in range(1, 12)]
    eta = 12 / np.sqrt(12 + 2 * sum((12 - k) * rho[k - 1]
                                    for k in range(1, 12)))
    return {"meses": T, "sr_mensual_x_raiz12": sr * np.sqrt(12),
            "se_iid": iid * np.sqrt(12),
            "se_no_normal": no_normal * np.sqrt(12),
            "eta": eta, "sr_anual_lo": sr * eta}

pd.DataFrame({k: error_sharpe(r) for k, r in rets.items()}).T.round(3)
```
```output
        meses  sr_mensual_x_raiz12  se_iid  se_no_normal    eta  sr_anual_lo
indice  251.0                0.549   0.220         0.264  4.242        0.672
tsmom   251.0                1.033   0.223         0.219  3.717        1.109
carry   251.0                0.409   0.219         0.239  2.935        0.347
```

*Fig. — Sharpe anual de las tres estrategias con su intervalo de ± 2 errores típicos (fórmula con asimetría y curtosis, 251 meses); la marca fina es el Sharpe anualizado con la corrección por autocorrelación de Lo (2002).* Tres intervalos horizontales. TSMOM en futuros, destacado: Sharpe 1,03 con un intervalo de 0,60 a 1,47, el único lejos del cero; su Sharpe corregido por autocorrelación sube a 1,11. Índice IDA: 0,55, de 0,02 a 1,08, el más ancho por la caída de marzo de 2020; corregido por autocorrelación sube a 0,67, porque tras las caídas vienen rebotes. Carry de divisas: 0,41, de −0,07 a 0,89, el único que incluye el cero; su Sharpe corregido por autocorrelación baja a 0,35.

Con 251 meses, los tres errores típicos están entre 0,219 y 0,264. La forma de la distribución sube dos: el del
índice, de 0,220 a 0,264, por el mes de marzo de 2020 (asimetría −2,15, curtosis 15,9), y el del carry, de 0,219 a
0,239. La autocorrelación separa a las tres. El $\eta$ del carry es 2,935, muy por debajo de 3,464: los meses malos
del carry vienen seguidos (los crashes del guion duran de uno a cuatro meses), y su Sharpe anual corregido baja de 0,41
a 0,35. El del índice es 4,242, por encima: sus autocorrelaciones de los retardos 3 a 10 son negativas, porque tras
cada caída viene un rebote, y su Sharpe anual corregido sube de 0,55 a 0,67. El de TSMOM es 3,717 (sube de 1,03 a
1,11). La misma corrección baja un Sharpe o lo sube según la serie.

La figura pone los intervalos de ± 2 errores típicos. Solo el de TSMOM queda lejos del cero (de 0,60 a 1,47). El del
índice lo roza (de 0,02 a 1,08) y el del carry lo incluye (de −0,07 a 0,89). Y eso que el carry tiene un efecto
plantado en los datos: 21 años de un Sharpe de 0,4 no bastan para distinguirlo del azar con esta regla. Cuando además
se han probado muchas variantes antes de quedarse con una, el listón sube; es el Deflated Sharpe de la lección 2.5.4.

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

Las métricas mensuales de esta sección parten de la misma serie diaria, agregada con `resample("ME")`: el Sharpe
mensual anualizado (0,409 en el carry) no es el diario anualizado (0,451). Ninguno es «el» Sharpe: informa siempre de
la frecuencia de cálculo, y prefiere la mensual para los errores típicos, porque la diaria arrastra más autocorrelación
y más curtosis.

</aside>

## Sortino, Calmar y Omega

El Sharpe penaliza igual las subidas que las bajadas. Sortino y Price (1994)[^6] proponen dividir solo por la
**desviación a la baja** respecto a un objetivo $\tau$:

$$
\text{Sortino} = \frac{\overline{r - \tau}}{\sqrt{\tfrac{1}{N}\sum_{t} \min(r_t - \tau,\ 0)^2}}\;\sqrt{n}
$$

La suma va sobre todos los periodos ($N$), no solo sobre los negativos: los días por encima del objetivo cuentan como
cero. Es la convención de empyrical y de quantstats. Si la distribución fuera simétrica, la desviación a la baja sería
$\sigma/\sqrt{2}$ y el Sortino, $\sqrt{2} \approx 1{,}41$ veces el Sharpe. El cociente entre los dos mide la forma:

```python
def sortino(r, n=252, objetivo=0.0):
    exceso = r - objetivo
    abajo = np.sqrt((exceso.clip(upper=0) ** 2).mean())
    return exceso.mean() / abajo * np.sqrt(n)

def mensual(r):
    return (1 + r).resample("ME").prod() - 1

pd.DataFrame({k: {"sharpe": sharpe(r), "sortino": sortino(r),
                  "cociente": sortino(r) / sharpe(r),
                  "cociente_mensual": sortino(mensual(r), 12)
                                      / sharpe(mensual(r), n=12)}
              for k, r in rets.items()}).T.round(3)
```
```output
        sharpe  sortino  cociente  cociente_mensual
indice   0.559    0.805     1.441             1.350
tsmom    1.026    1.527     1.489             1.901
carry    0.451    0.634     1.405             1.391
```

En diario, los tres cocientes están entre 1,41 y 1,49, cerca de $\sqrt{2}$: un día no tiene tiempo de mostrar la
forma de la estrategia. En mensual aparece: TSMOM llega a 1,90, porque sus meses buenos son más grandes que los malos;
el carry baja a 1,39 y el índice a 1,35, porque sus peores meses (los crashes y las crisis) son más grandes que los
mejores. El Sortino recoge la asimetría, pero solo si se mide en una
frecuencia en la que exista.

El **Calmar** divide el CAGR entre el drawdown máximo en valor absoluto. Es la cifra que más mira un inversor de un
fondo de futuros: cuánto gana por cada punto de la peor caída. El **Omega** de Keating y Shadwick (2002)[^7] compara
toda la masa de la distribución por encima de un umbral $L$ con la masa por debajo:

$$
\Omega(L) = \frac{\sum_t (r_t - L)^{+}}{\sum_t (L - r_t)^{+}}
$$

Un Omega mayor que 1 significa que, en suma, se gana más por encima del umbral de lo que se pierde por debajo.

```python
def omega(r, umbral=0.0):
    d = r - umbral
    return d.clip(lower=0).sum() / -d.clip(upper=0).sum()

u5 = 1.05 ** (1 / 252) - 1      # 5 % anual, por sesión
pd.DataFrame({k: {"diaria_0": omega(r), "diaria_5%": omega(r, u5),
                  "mensual_0": omega(mensual(r))}
              for k, r in rets.items()}).T.round(3)
```
```output
        diaria_0  diaria_5%  mensual_0
indice     1.115      1.064      1.609
tsmom      1.183      1.108      2.153
carry      1.079      0.985      1.390
```

Omega depende mucho de la frecuencia y del umbral. En diario, con umbral cero, los tres están entre 1,08 y 1,18:
cada día la media es diminuta frente a la dispersión. Con un umbral del 5 % anual (convertido a su equivalente por
sesión, como hace empyrical), el carry baja de 1: no supera ese objetivo. En mensual, TSMOM (2,15) se separa del índice
y del carry (1,61 y 1,39). Un Omega sin su umbral y su frecuencia no se puede comparar con otro.

## Drawdown: profundidad, duración y Ulcer Index

El drawdown de cada día es la caída del capital desde su máximo anterior (lección 1.6.1). Hay un detalle al calcularlo
desde rendimientos: el capital empieza en 1 **antes** del primer día. Si el primer rendimiento es negativo, el máximo
de `cumprod()` arranca por debajo de 1 y ese primer día no cuenta como caída; `clip(lower=1.0)` lo corrige.
quantstats añade una fecha ficticia con el valor inicial para lo mismo.

```python
def drawdown(r):
    capital = (1 + r).cumprod()
    pico = capital.cummax().clip(lower=1.0)   # se empieza en 1
    return capital / pico - 1

def episodios(r):
    dd = drawdown(r)
    bajo = dd < 0
    grupo = (~bajo).cumsum()[bajo]
    filas = []
    for _, e in dd[bajo].groupby(grupo):
        sig = dd.index.searchsorted(e.index[-1], side="right")
        filas.append({
            "inicio": e.index[0].date(), "minimo": e.idxmin().date(),
            "recupera": dd.index[sig].date() if sig < len(dd) else None,
            "profundidad": e.min(), "sesiones": len(e),
            "anos": ((dd.index[min(sig, len(dd) - 1)] - e.index[0])
                     .days / 365.25)})
    return pd.DataFrame(filas)

ep_carry = episodios(rets["carry"])
ep_carry.sort_values("profundidad").head(3).round(3)
```
```output
         inicio      minimo    recupera  profundidad  sesiones   anos
49   2008-08-29  2011-03-31  2018-06-15       -0.479      2555  9.793
68   2019-07-29  2020-03-31  2022-04-13       -0.235       707  2.708
109  2025-03-17  2025-07-03        None       -0.108       208  0.791
```

El peor episodio del carry empieza el 29 de agosto de 2008, toca fondo el 31 de marzo de 2011 en −47,9 % y no
recupera el máximo hasta el 15 de junio de 2018: 2.555 sesiones, 9,8 años bajo el agua. El crash de 2008 deja la
caída en el −24,7 % en diciembre; los crashes de 2010 y 2011 la duplican. Para un inversor, la profundidad es la mitad
del daño: la otra mitad es una década sin máximos nuevos.

El **Ulcer Index** de Peter Martin (1987)[^8] combina las dos dimensiones en una cifra: la raíz de la media de los
drawdowns al cuadrado,

$$
UI = \sqrt{\frac{1}{N}\sum_{t=1}^{N} DD_t^{2}}
$$

Un drawdown profundo y largo pesa mucho más que uno igual de profundo pero breve, porque se suma muchos días. El
**ratio de Martin** (Ulcer Performance Index) divide el exceso de rentabilidad anual entre el Ulcer Index, como el
Sharpe divide por la desviación típica.

```python
def ulcer(r):
    return np.sqrt((drawdown(r) ** 2).mean())

def martin(r, rf_anual=0.0):
    return (cagr_fechas(r) - rf_anual) / ulcer(r)

def resumen_dd(r):
    e = episodios(r)
    return {"dd_max": e["profundidad"].min(),
            "sesiones_max": e["sesiones"].max(),
            "anos_max": e["anos"].max(),
            "bajo_el_agua": (drawdown(r) < 0).mean(),
            "ulcer": ulcer(r), "martin": martin(r),
            "calmar": cagr_fechas(r) / -drawdown(r).min()}

pd.DataFrame({k: resumen_dd(r) for k, r in rets.items()}).T.round(3)
```
```output
        dd_max  sesiones_max  anos_max  bajo_el_agua  ulcer  martin  calmar
indice  -0.622         959.0     3.817         0.921  0.178   0.545   0.156
tsmom   -0.194         675.0     2.683         0.914  0.064   1.931   0.639
carry   -0.479        2555.0     9.793         0.958  0.213   0.182   0.081
```

*Fig. — Anatomía de un drawdown: el carry de divisas de 2005 a 2025, con su profundidad máxima, la duración del peor episodio y el Ulcer Index.* Área del drawdown del carry de divisas por semanas. Hasta agosto de 2008 apenas baja del 5 %; el crash lo lleva al −24,7 % en diciembre y la caída sigue hasta el máximo, destacado, de −47,9 % en marzo de 2011. Una llave encima marca el episodio completo, de agosto de 2008 a junio de 2018: 9,8 años bajo el agua. Una línea discontinua en −21,3 % marca el Ulcer Index, la raíz de la media de los drawdowns al cuadrado. Después hay otra caída del −23,5 % en 2020.

La figura dibuja la anatomía del peor drawdown del carry: la profundidad máxima en ámbar, la llave de los 9,8 años bajo
el agua y el Ulcer Index, 21,3 %, como una línea: es el nivel de drawdown «típico» en media cuadrática. Las tres
estrategias pasan más del 91 % de las sesiones por debajo de su máximo anterior; lo que las distingue es cuánto y
durante cuánto tiempo.

El índice cae más que el carry: su drawdown máximo es del −62,2 % (del 20 de noviembre de 2019 al 19 de mayo de 2020),
y el de 2008, del −56,0 % (de octubre de 2007 a abril de 2009). Pero se recupera de los dos en algo más de tres años:
en septiembre de 2023 (3,8 años) y en noviembre de 2010 (3,1 años). El carry, con una caída menor, pasa 9,8 años bajo
el agua. Profundidad y duración son riesgos distintos y hay que informar de los dos.

*Fig. — Cuatro métricas de las tres estrategias, febrero de 2005 a 2025: el carry FX se acerca al índice en Sharpe, pero su Calmar es la mitad y su Ulcer, mayor.* Cuatro paneles de barras horizontales, uno por métrica, con índice, TSMOM y carry FX. Sharpe: 0,56, 1,03 y 0,45. Sortino: 0,81, 1,53 y 0,63. Calmar: 0,16, 0,64 y 0,08, destacado. Ulcer Index: 17,8 %, 6,4 % y 21,3 %. TSMOM gana en las cuatro; el carry se acerca al índice en Sharpe, pero su Calmar es la mitad y su Ulcer, mayor, con menos de la mitad de volatilidad.

La comparación de la figura resume la lección. En Sharpe, el índice y el carry están cerca (0,56 y 0,45). En Calmar,
el carry tiene la mitad (0,08 frente a 0,16), y en Ulcer, más (21,3 % frente a 17,8 %), con menos de la mitad de
volatilidad que el índice (9,1 % frente a 20,3 %): su drawdown máximo equivale a 5,3 veces su volatilidad anual, y el
del índice, a 3,1. Es la firma de una estrategia con asimetría negativa y pérdidas que se encadenan: la desviación
típica la hace parecer segura hasta el día del crash. TSMOM gana en las cuatro métricas; en estos datos sintéticos,
porque el término de momentum está plantado en el rendimiento esperado de cada futuro.

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

El drawdown máximo crece con la longitud de la muestra: cuanto más tiempo, más probable es encontrar una crisis peor.
En el carry, el de 2005 a 2010 es del −44,3 % y el de toda la muestra, del −47,9 %. Un Calmar de tres años y uno de
veinte no se pueden comparar, y un backtest corto que no incluye ningún crash del tipo de su estrategia promete un
Calmar que no existe.

</aside>

## Contra empyrical-reloaded

empyrical-reloaded es la librería de métricas que usan Zipline Reloaded y pyfolio-reloaded (código en su repositorio,
Apache 2.0). Comparar cada función propia con la suya es la prueba más barata de que las fórmulas están bien
implementadas:

```python
import empyrical as ep

def comparar(r):
    return pd.Series({
        "sharpe": sharpe(r) - ep.sharpe_ratio(r),
        "sortino": sortino(r) - ep.sortino_ratio(r),
        "omega": omega(r) - ep.omega_ratio(r),
        "dd_max": drawdown(r).min() - ep.max_drawdown(r),
        "vol": vol_anual(r) - ep.annual_volatility(r),
        "cagr_252": cagr_sesiones(r) - ep.cagr(r),
        "cagr_fechas": cagr_fechas(r) - ep.cagr(r),
    })

dif_max = pd.DataFrame({k: comparar(r) for k, r in rets.items()})
dif_max.abs().max(axis=1)
```
```output
sharpe         1.665335e-15
sortino        2.886580e-15
omega          2.220446e-16
dd_max         5.551115e-17
vol            0.000000e+00
cagr_252       0.000000e+00
cagr_fechas    1.358127e-03
dtype: float64
```

Sharpe, Sortino, Omega, drawdown máximo y volatilidad coinciden hasta el error de redondeo de la coma flotante
($10^{-15}$). El CAGR propio con 252 sesiones también. El CAGR por fechas difiere en 0,0014, la diferencia del carry
de la sección del CAGR: empyrical no se equivoca, usa otra convención (`annualization=261` en sus funciones la
cambiaría). Ulcer, duración y ratio de Martin no están en empyrical; quantstats sí tiene un `ulcer_index`, y la
lección 2.6.3 lo compara con este.

## Qué métrica para qué forma de rentabilidad

- **Para una estrategia simétrica y de colas finas**, Sharpe y Sortino cuentan casi lo mismo y el Sharpe es el
  estándar para comparar. Una cartera de acciones lo es casi siempre, pero no en sus crisis: el índice de esta lección
  tiene una asimetría mensual de −2,15 por un solo mes.
- **Para una estrategia con asimetría negativa** (carry, venta de volatilidad, reversión con stops lejanos), el Sharpe
  es optimista. Mira Calmar, Ulcer, duración del drawdown y el Sortino mensual; y desconfía de cualquier backtest que no
  incluya un crash.
- **Para una estrategia con asimetría positiva** (tendencia), el Sharpe es conservador y lo que pesa es la
  duración de las rachas planas: los 2,7 años del peor episodio de TSMOM son el momento en que se abandona un sistema.
- **Siempre**: el mismo periodo para todas, la frecuencia y la convención de anualización a la vista, el tipo libre
  solo donde corresponde y el error típico junto a cada Sharpe.

## Ejercicios

1. Calcula el Calmar del carry con datos hasta el 31 de diciembre de 2007 y con toda la muestra. ¿Qué habría concluido
   alguien que lo evaluara a principios de 2008?

**Solución.**

```python
for fin in ("2007-12-31", "2025-12-31"):
    r = rets["carry"].loc[:fin]
    dd = drawdown(r).min()
    print(f"hasta {fin}: CAGR {cagr_fechas(r):.2%}, "
          f"DD máx {dd:.1%}, Calmar {cagr_fechas(r) / -dd:.2f}, "
          f"Sharpe {sharpe(r, n=261):.2f}")
```
```output
hasta 2007-12-31: CAGR 11.09%, DD máx -9.1%, Calmar 1.22, Sharpe 1.30
hasta 2025-12-31: CAGR 3.89%, DD máx -47.9%, Calmar 0.08, Sharpe 0.46
```

Hasta 2007, el carry tenía un CAGR del 11,09 %, un drawdown máximo del −9,1 %, un Calmar de 1,22 y un Sharpe de
1,30: una estrategia excelente en cualquier tabla. Con toda la muestra, el Calmar baja a 0,08 y el Sharpe a 0,46. Nada
en los tres años previos anunciaba el crash: la asimetría negativa solo se ve cuando ocurre. Por eso un backtest de
carry tiene que incluir periodos de crisis, y por eso el Calmar de una muestra corta sin crash no dice nada.

2. Martin recomienda calcular el Ulcer Index con datos semanales. Compáralo con el diario en las tres estrategias,
   tomando el último capital de cada semana.

**Solución.**

```python
def ulcer_semanal(r):
    capital = (1 + r).cumprod().resample("W-FRI").last()
    dd = capital / capital.cummax().clip(lower=1.0) - 1
    return np.sqrt((dd ** 2).mean())

pd.DataFrame({k: {"diario": ulcer(r), "semanal": ulcer_semanal(r)}
              for k, r in rets.items()}).T.round(4)
```
```output
        diario  semanal
indice  0.1781   0.1719
tsmom   0.0642   0.0615
carry   0.2133   0.2117
```

Con datos semanales el Ulcer Index baja muy poco (de 17,81 % a 17,19 % en el índice, de 6,42 % a 6,15 % en TSMOM,
de 21,33 % a 21,17 % en el carry): como dice Martin, es casi independiente de la frecuencia, al revés que la
desviación típica, que hay que anualizar. Baja un poco porque el cierre del viernes se salta algunos mínimos de mitad
de semana.

3. Un gestor presenta un Sharpe de 1,2 calculado con 36 meses de rendimientos mensuales multiplicados por √12. ¿Qué
   cuatro preguntas le harías antes de creértelo?

**Solución.**

Primera, si ha restado el tipo libre y cuál: con tipos al 4 o 5 %, no restarlo infla mucho el Sharpe de una estrategia
de baja volatilidad. Segunda, cuál es su error típico: con 3 años, $1/\sqrt{3} \approx 0{,}58$, así que el intervalo de
± 2 errores típicos va de 0,05 a 2,35 aproximadamente y el dato es compatible con una estrategia mediocre. Tercera, si
los rendimientos mensuales tienen autocorrelación: una cartera con activos poco líquidos o valorados con retraso tiene
autocorrelación positiva, y el factor $\eta(12)$ de Lo rebaja el Sharpe anual. Cuarta, qué forma tiene la
distribución y qué pasó en el peor mes: si la estrategia vende volatilidad o cobra carry, 36 meses sin crash dan un
Sharpe alto que la asimetría negativa desmentirá; pide el drawdown máximo, su duración y el Sortino mensual.

[^1]: 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>.
[^2]: Markus K. Brunnermeier, Stefan Nagel y Lasse H. Pedersen, «Carry Trades and Currency Crashes», NBER
    Macroeconomics Annual 23, 313-348 (2008), <https://doi.org/10.1086/593088>.
[^3]: William F. Sharpe, «The Sharpe Ratio», The Journal of Portfolio Management 21(1), 49-58 (1994): la versión
    «ex post» es la media de la diferencia entre la rentabilidad del fondo y la de la referencia (aquí, el tipo libre)
    dividida por su desviación típica.
[^4]: Andrew W. Lo, «The Statistics of Sharpe Ratios», Financial Analysts Journal 58(4), 36-52 (2002).
[^5]: La versión con asimetría y curtosis se debe a Mertens (2002); está reproducida en David H. Bailey y Marcos López de
    Prado, «The Sharpe ratio efficient frontier», The Journal of Risk 15(2), 3-44 (2012),
    <https://doi.org/10.21314/JOR.2012.255>, la base del Deflated Sharpe de la lección 2.5.4.
[^6]: Frank A. Sortino y Lee N. Price, «Performance Measurement in a Downside Risk Framework», The Journal of Investing
    3(3), 59-64 (1994).
[^7]: Con Keating y William F. Shadwick, «A Universal Performance Measure», The Journal of Performance Measurement
    6(3) (2002).
[^8]: Peter G. Martin, «Ulcer Index: An Alternative Approach to the Measurement of Investment Risk & Risk-Adjusted
    Performance», <https://www.tangotools.com/ui/ui.htm>: el autor lo desarrolló en 1987 y lo publicó en 1989; define el
    UPI con la rentabilidad total anualizada menos la del activo libre de riesgo.

## Autoevaluación

1. El carry de divisas cotiza 261 días al año. ¿Por qué su CAGR calculado con `empyrical.cagr` (3,75 %) es menor que el calculado con las fechas (3,89 %)?

   - A. Porque empyrical resta el tipo libre
   - B. Porque empyrical cuenta los años como sesiones entre 252, y 5.457 días laborables son 21,7 «años» de 252 cuando el calendario solo tiene 20,9
   - C. Porque empyrical usa rentabilidades logarítmicas
   - D. Porque la serie tiene huecos en los festivos

   *Respuesta B: Porque empyrical cuenta los años como sesiones entre 252, y 5.457 días laborables son 21,7 «años» de 252 cuando el calendario solo tiene 20,9.* El mismo crecimiento total repartido entre más años da una tasa menor. Para el CAGR, cuenta siempre los años con las fechas; para anualizar medias y volatilidades, usa las sesiones reales por año del mercado.

2. TSMOM en futuros tiene un Sharpe de 1,026. Si le sumas el interés que cobra el colateral y calculas el Sharpe sin restar el tipo libre, sale 1,173. ¿Cuál es el correcto?

   - A. 1,173, porque es la rentabilidad que de verdad se cobra
   - B. 1,026: el Sharpe mide el exceso sobre el tipo libre, y la serie de futuros ya es un exceso; sobre la rentabilidad total hay que restar el tipo libre, y se vuelve a 1,026
   - C. La media de los dos
   - D. Ninguno: con futuros el Sharpe no tiene sentido

   *Respuesta B: 1,026: el Sharpe mide el exceso sobre el tipo libre, y la serie de futuros ya es un exceso; sobre la rentabilidad total hay que restar el tipo libre, y se vuelve a 1,026.* Sin restar el tipo libre, cualquier estrategia que deje el dinero en letras parece tener ventaja. Con un tipo medio del 1,79 %, el error suma 0,15 al Sharpe de TSMOM.

3. Con 251 meses de datos, el error típico de un Sharpe anual ronda 0,22. ¿Qué implica para el carry de divisas, con un Sharpe mensual anualizado de 0,41?

   - A. Que su ventaja está demostrada al 95 %
   - B. Que su intervalo de ± 2 errores típicos va de −0,07 a 0,89: con 21 años de datos, ese Sharpe no se distingue de cero
   - C. Que el error típico no depende de los años de datos
   - D. Que el Sharpe real es exactamente 0,41

   *Respuesta B: Que su intervalo de ± 2 errores típicos va de −0,07 a 0,89: con 21 años de datos, ese Sharpe no se distingue de cero.* El error típico del Sharpe anual es del orden de 1/√años. Con su asimetría negativa y su curtosis, el del carry sube a 0,239: el intervalo incluye el cero, aunque el efecto esté plantado en los datos.

4. El índice IDA y el carry FX tienen un Sharpe parecido (0,56 y 0,45). ¿Por qué el Calmar del carry es la mitad?

   - A. Porque el carry tiene más volatilidad
   - B. Porque el carry gana un 3,9 % al año y, con una volatilidad del 9 %, cae un 47,9 % en su peor episodio, 5,3 veces su volatilidad: sus pérdidas llegan en crashes que la desviación típica no anticipa; el índice cae más (62,2 %), pero gana un 9,7 % al año
   - C. Porque el Calmar penaliza los mercados de divisas
   - D. Por un error de cálculo

   *Respuesta B: Porque el carry gana un 3,9 % al año y, con una volatilidad del 9 %, cae un 47,9 % en su peor episodio, 5,3 veces su volatilidad: sus pérdidas llegan en crashes que la desviación típica no anticipa; el índice cae más (62,2 %), pero gana un 9,7 % al año.* El Sharpe solo ve la dispersión. Los dos tienen asimetría mensual negativa, pero el índice rebota tras cada crisis y el carry encadena meses malos (su η de Lo es 2,94, por debajo de √12). El Calmar, el Ulcer (21,3 % frente a 17,8 %) y la duración del drawdown (9,8 años frente a 3,8) sí recogen esa trayectoria.

5. ¿Qué mide el Ulcer Index?

   - A. La peor pérdida diaria
   - B. La raíz de la media de los drawdowns al cuadrado: crece con la profundidad y con el tiempo que se pasa bajo el agua
   - C. La desviación típica de las rentabilidades negativas
   - D. El número de episodios de drawdown

   *Respuesta B: La raíz de la media de los drawdowns al cuadrado: crece con la profundidad y con el tiempo que se pasa bajo el agua.* Un drawdown profundo pero breve y uno moderado pero larguísimo pueden dar el mismo Ulcer. Es la métrica que más se parece a lo que siente quien sigue la estrategia; Martin propone dividir por ella el exceso de rentabilidad anual (ratio de Martin o UPI).

## Ideas clave

- Cada métrica trae una convención: años por fechas para el CAGR; sesiones reales por año (252 en bolsa, 261 en divisas) para medias y volatilidades; y un tipo libre que se resta solo si la serie no es ya un exceso.
- Un Sharpe es una estimación con error: unas 1/√años unidades. Con 21 años, ± 0,44 a dos errores típicos; la asimetría negativa y la autocorrelación lo empeoran.
- Sharpe y Sortino miran la dispersión; Calmar, duración y Ulcer miran la trayectoria. Un carry con asimetría negativa se acerca en Sharpe a un índice (0,45 y 0,56), tiene la mitad de Calmar y más Ulcer con menos de la mitad de volatilidad.
- El drawdown tiene profundidad y duración: el del índice es más profundo (−62,2 %) y se recupera en 3,8 años; el del carry (−47,9 %) dura 9,8.
- Implementadas desde cero, Sharpe, Sortino, Omega, volatilidad y drawdown coinciden con empyrical-reloaded hasta el redondeo; el CAGR solo coincide si se cuentan los años como él.
- Los datos son sintéticos y los efectos están plantados: estas cifras enseñan a medir, no prueban que TSMOM o el carry funcionen en mercados reales.

## Recursos

### Research

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

### Documentación

- [Peter G. Martin: Ulcer Index y Ulcer Performance Index (definición y algoritmo)](https://www.tangotools.com/ui/ui.htm) · tangotools.com
- [empyrical-reloaded: métricas de riesgo y rendimiento (código fuente)](https://github.com/stefan-jansen/empyrical-reloaded) · github.com

### Libros

- [Andrew W. Lo, «The Statistics of Sharpe Ratios», Financial Analysts Journal 58(4), 36-52 (2002)](https://doi.org/10.2469/faj.v58.n4.2453) · doi.org
- [William F. Sharpe, «The Sharpe Ratio», The Journal of Portfolio Management 21(1), 49-58 (1994)](https://doi.org/10.3905/jpm.1994.409501) · doi.org
- [Frank A. Sortino y Lee N. Price, «Performance Measurement in a Downside Risk Framework», The Journal of Investing 3(3), 59-64 (1994)](https://doi.org/10.3905/joi.3.3.59) · doi.org

### Datos

- [Datos del nivel 2: carry de divisas, 2005-2025 (Parquet)](https://pythonparatrading.com/curso/datos/nivel-2/fx2_carry.parquet) · Descarga · fx2_carry.parquet

Lección anterior: [2.5.8 · Checklist de «backtest honesto» antes de pasar a paper trading](https://pythonparatrading.com/curso-python-trading/nivel-2/checklist-de-backtest-honesto/)

Lección siguiente: [2.6.2 · Win rate, payoff, expectativa, alpha, beta, information ratio, turnover y capacidad](https://pythonparatrading.com/curso-python-trading/nivel-2/metricas-de-operaciones-alpha-beta-y-capacidad/)
