Lección 2.6.1 · 65 min

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.

Fig. — Tres estrategias sobre datos sintéticos del nivel 2, de febrero de 2005 a 2025: capital acumulado en escala logarítmica
Lecciones del nivel 2
  1. 2.1 · Estadística y econometría para trading

    1. 2.1.1 Contrastes de hipótesis, p-valores, bootstrap y regresión con errores robustos
    2. 2.1.2 Estacionariedad: ADF, KPSS, diferenciación y diferenciación fraccional
    3. 2.1.3 Modelos ARMA y ARIMA: autocorrelación simple (ACF) y parcial (PACF)
    4. 2.1.4 Modelos de volatilidad: ARCH, GARCH, EGARCH y GJR con la librería arch
    5. 2.1.5 Cointegración: Engle-Granger, Johansen y vida media de la reversión
    6. 2.1.6 Filtro de Kalman para hedge ratios dinámicos
    7. 2.1.7 Detección de regímenes: modelos de Markov ocultos y cambios estructurales
    8. 2.1.8 El problema de las pruebas múltiples: Bonferroni, FDR y el «factor zoo»
  2. 2.2 · Anatomía de un sistema de trading

    1. 2.2.1 Hipótesis de ventaja: riesgo, comportamiento, estructura e información
    2. 2.2.2 Universo, señal, filtro, entrada, salida, sizing y gestión del riesgo
    3. 2.2.3 Frecuencias: intradía, swing y posicional, y la capacidad de una estrategia
    4. 2.2.4 Diario de investigación y registro de experimentos contra el data snooping
  3. 2.3 · Familias de estrategias

    1. 2.3.1 Seguimiento de tendencia y momentum
    2. 2.3.2 Reversión a la media: RSI y Bollinger, reversión intradía y huecos
    3. 2.3.3 Pairs trading y arbitraje estadístico: distancia, cointegración y Kalman
    4. 2.3.4 Carry en divisas, futuros y bonos; estacionalidad y efectos calendario
    5. 2.3.5 Factores (value, calidad, baja volatilidad, tamaño) y estrategias de eventos
    6. 2.3.6 Introducción al market making: inventario, spread y selección adversa (Avellaneda-Stoikov)
    7. 2.3.7 Estrategias específicas de cripto: momentum, arbitraje de funding y basis
  4. 2.4 · Backtesting: motores y paradigmas

    1. 2.4.1 Backtest vectorizado desde cero: matrices, desfase, costes y límites
    2. 2.4.2 Backtest por eventos desde cero: bucle, cartera, órdenes y fills
    3. 2.4.3 vectorbt: barridos masivos de parámetros y simulación de carteras
    4. 2.4.4 backtesting.py y backtrader: dos motores didácticos y su estado
    5. 2.4.5 NautilusTrader: arquitectura, datos, estrategias y el mismo código en backtest y en real
    6. 2.4.6 LEAN/QuantConnect y Zipline Reloaded; bt para asignación de activos
    7. 2.4.7 Modelado de costes: comisiones, horquilla, impacto, financiación y préstamo de títulos
    8. 2.4.8 Modelado de fills: órdenes limitadas, cola, fills parciales, huecos y stops
  5. 2.5 · Sesgos y validación

    1. 2.5.1 Sesgos de backtest: anticipación, supervivencia, selección y narrativa
    2. 2.5.2 In-sample y out-of-sample: walk-forward anclado y móvil
    3. 2.5.3 Validación cruzada combinatoria purgada para estrategias
    4. 2.5.4 Deflated Sharpe Ratio y Probability of Backtest Overfitting (PBO)
    5. 2.5.5 Monte Carlo: remuestreo de trades, bootstrap de bloques y permutaciones de señales
    6. 2.5.6 Sensibilidad de parámetros: mesetas frente a picos, robustez entre activos y periodos
    7. 2.5.7 Datos sintéticos para pruebas de estrés: GBM, GARCH y bootstrapping
    8. 2.5.8 Checklist de «backtest honesto» antes de pasar a paper trading
  6. 2.6 · Métricas de rendimiento

    1. 2.6.1 CAGR, Sharpe, Sortino, Calmar, Omega, drawdown y Ulcer Index
    2. 2.6.2 Win rate, payoff, expectativa, alpha, beta, information ratio, turnover y capacidad
    3. 2.6.3 Informes con QuantStats o pyfolio-reloaded y tearsheets propios

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)1Nota 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.. 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.
IN [01]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))
OUT [01]Tres estrategias en datos del nivel 2
        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.

RendimientosCrecimientoDispersiónTrayectoriaCAGRSharpe, SortinoOmegaDD, Calmar, Ulcer
Fuente 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:

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

con DD 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.

IN [02]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)
OUT [02]CAGR por fechas y por sesiones
        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 σ2/2\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.

crecimientomedia o volbolsa EE. UU.divisascripto¿Qué se anualiza?Años por fechas¿Mercado?252 sesiones261 días365 días
Fuente 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ípica3Nota 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.:

SR=r−rf‾σ(r−rf) nSR = \frac{\overline{r - r_f}}{\sigma(r - r_f)}\,\sqrt{n}

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

IN [03]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)
OUT [03]Volatilidad y Sharpe
          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 % (261/252\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:

IN [04]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}")
OUT [04]Sharpe con el tipo libre
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)4Nota 4 · Andrew W. Lo, «The Statistics of Sharpe Ratios», Financial Analysts Journal 58(4), 36-52 (2002). lo deriva para rendimientos independientes y normales:

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

con SRSR y TT 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/A1/\sqrt{A}, con AA 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 γ3\gamma_3 y curtosis γ4\gamma_4 (no de exceso), el error es5Nota 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.

SE(SR^)≈1−γ3 SR+γ4−14 SR2T−1SE(\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 12\sqrt{12} supone meses independientes. Lo (2002) da el factor correcto,

SRanual=η(q) SRmensual,η(q)=qq+2∑k=1q−1(q−k) ρkSR_{\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=12q = 12 y ρk\rho_k la autocorrelación de los rendimientos mensuales en el retardo kk. Con autocorrelación positiva, η(12)\eta(12) es menor que 12≈3,464\sqrt{12} \approx 3{,}464 y el Sharpe anual baja.

IN [05]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)
OUT [05]Error típico del Sharpe (Lo, 2002)
        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)

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.

Sortino, Calmar y Omega

El Sharpe penaliza igual las subidas que las bajadas. Sortino y Price (1994)6Nota 6 · Frank A. Sortino y Lee N. Price, «Performance Measurement in a Downside Risk Framework», The Journal of Investing 3(3), 59-64 (1994). proponen dividir solo por la desviación a la baja respecto a un objetivo τ\tau:

Sortino=r−τ‾1N∑tmin⁡(rt−τ, 0)2  n\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 (NN), 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 σ/2\sigma/\sqrt{2} y el Sortino, 2≈1,41\sqrt{2} \approx 1{,}41 veces el Sharpe. El cociente entre los dos mide la forma:

IN [06]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)
OUT [06]Sortino
        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 2\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)7Nota 7 · Con Keating y William F. Shadwick, «A Universal Performance Measure», The Journal of Performance Measurement 6(3) (2002). compara toda la masa de la distribución por encima de un umbral LL con la masa por debajo:

Ω(L)=∑t(rt−L)+∑t(L−rt)+\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.

IN [07]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)
OUT [07]Omega
        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.

IN [08]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)
OUT [08]Drawdown con el punto de partida
         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)8Nota 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. combina las dos dimensiones en una cifra: la raíz de la media de los drawdowns al cuadrado,

UI=1N∑t=1NDDt2UI = \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.

IN [09]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)
OUT [09]Ulcer Index y ratio de Martin
        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

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

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.

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:

IN [10]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)
OUT [10]Contra empyrical-reloaded
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−1510^{-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
IN [11]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}")
OUT [11]Calmar antes y después del crash
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.

  1. 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
IN [12]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)
OUT [12]Ulcer diario y semanal
        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.

  1. 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/3≈0,581/\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 η(12)\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.

Footnotes

  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. ↩

Respuesta plegada en cada pregunta

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 %)?

    • APorque empyrical resta el tipo libre
    • BPorque 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
    • CPorque empyrical usa rentabilidades logarítmicas
    • DPorque la serie tiene huecos en los festivos
    Ver respuesta

    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?

    • A1,173, porque es la rentabilidad que de verdad se cobra
    • B1,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
    • CLa media de los dos
    • DNinguno: con futuros el Sharpe no tiene sentido
    Ver respuesta

    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?

    • AQue su ventaja está demostrada al 95 %
    • BQue 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
    • CQue el error típico no depende de los años de datos
    • DQue el Sharpe real es exactamente 0,41
    Ver respuesta

    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?

    • APorque el carry tiene más volatilidad
    • BPorque 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
    • CPorque el Calmar penaliza los mercados de divisas
    • DPor un error de cálculo
    Ver respuesta

    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?

    • ALa peor pérdida diaria
    • BLa raíz de la media de los drawdowns al cuadrado: crece con la profundidad y con el tiempo que se pasa bajo el agua
    • CLa desviación típica de las rentabilidades negativas
    • DEl número de episodios de drawdown
    Ver respuesta

    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