Saltar al contenido principal
Módulo 5 · Fundamentos

Casos prácticos

Métricas de Riesgo y Performance

Módulo 5: Casos Prácticos Resueltos

Métricas de Riesgo y Performance — Laboratorio en Python


📋 Introducción

Estos ejercicios convierten las métricas del Módulo 5 en código que puedes ejecutar sobre datos reales. Calcularás Sharpe, Sortino, Calmar, los tres tipos de VaR, el CVaR y el máximo drawdown. Ejecuta cada bloque y experimenta.

Requisitos Previos

pip install numpy pandas matplotlib scipy yfinance

🧪 Caso Práctico 1: Calcular los Ratios de Performance

Objetivo

Implementar Sharpe, Sortino y Calmar sobre un activo real.

Código

import yfinance as yf
import numpy as np

# Descargar datos
datos = yf.download("SPY", start="2018-01-01", end="2024-01-01")
retornos = datos["Close"].pct_change().dropna()

DIAS = 252
rf_anual = 0.03  # tasa libre de riesgo anual
rf_diario = rf_anual / DIAS

# --- SHARPE RATIO ---
exceso = retornos - rf_diario
sharpe = (exceso.mean() / retornos.std()) * np.sqrt(DIAS)

# --- SORTINO RATIO (solo downside) ---
downside = retornos[retornos < 0]
downside_dev = downside.std()
sortino = (exceso.mean() / downside_dev) * np.sqrt(DIAS)

# --- MÁXIMO DRAWDOWN (para Calmar) ---
acumulado = (1 + retornos).cumprod()
max_movil = acumulado.cummax()
drawdown = (acumulado - max_movil) / max_movil
max_dd = drawdown.min()

# --- CALMAR RATIO ---
retorno_anual = retornos.mean() * DIAS
calmar = retorno_anual / abs(max_dd)

print("=== RATIOS DE PERFORMANCE (SPY) ===")
print(f"Retorno anualizado: {retorno_anual:.2%}")
print(f"Volatilidad anual:  {retornos.std()*np.sqrt(DIAS):.2%}")
print(f"Sharpe Ratio:       {sharpe:.2f}")
print(f"Sortino Ratio:      {sortino:.2f}")
print(f"Máximo Drawdown:    {max_dd:.2%}")
print(f"Calmar Ratio:       {calmar:.2f}")

Interpretación

  • Sharpe vs. Sortino: el Sortino suele ser mayor que el Sharpe, porque solo penaliza las pérdidas (no la volatilidad al alza)
  • Calmar: relaciona el retorno con el peor drawdown; valores >1 indican buen equilibrio
  • Experimenta: prueba con un activo más volátil (como una cripto o una tecnológica) y observa cómo empeoran todos los ratios

🧪 Caso Práctico 2: Visualizar el Drawdown

Objetivo

Graficar la curva de drawdown y entender visualmente las peores rachas.

Código

import matplotlib.pyplot as plt

fig, axes = plt.subplots(2, 1, figsize=(13, 8), sharex=True)

# Valor acumulado de la inversión
axes[0].plot(acumulado.index, acumulado, color="steelblue", linewidth=1)
axes[0].plot(max_movil.index, max_movil, color="green", linestyle="--",
             alpha=0.5, label="Máximo histórico")
axes[0].set_title("Valor acumulado de 1€ invertido")
axes[0].set_ylabel("Valor")
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# Curva de drawdown
axes[1].fill_between(drawdown.index, drawdown*100, 0, color="crimson", alpha=0.4)
axes[1].plot(drawdown.index, drawdown*100, color="darkred", linewidth=0.8)
axes[1].set_title(f"Drawdown (máximo: {max_dd:.1%})")
axes[1].set_ylabel("Drawdown (%)")
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# Calcular el tiempo de recuperación del peor drawdown
fecha_min = drawdown.idxmin()
print(f"Peor drawdown alcanzado el: {fecha_min.date()}")

Interpretación

Qué observas:

  • El gráfico superior muestra cómo crece la inversión, con la línea de “máximo histórico” marcando los picos
  • El gráfico inferior (drawdown) muestra las caídas desde cada pico: cada valle es una racha de pérdidas
  • El valle más profundo es el máximo drawdown (probablemente el crash de COVID en marzo 2020)

Lección: el drawdown captura el “dolor sostenido” que la volatilidad no muestra. Una caída del 34% como la de COVID requiere un +52% para recuperarse — visualmente entiendes por qué los inversores entran en pánico.


🧪 Caso Práctico 3: VaR por los Tres Métodos

Objetivo

Calcular el VaR con los tres enfoques y compararlos.

Código

import numpy as np
from scipy import stats

valor_cartera = 1_000_000  # 1 millón de euros
confianza = 0.95
retornos = datos["Close"].pct_change().dropna()

# --- 1. VaR PARAMÉTRICO (asume normal) ---
mu = retornos.mean()
sigma = retornos.std()
z = stats.norm.ppf(1 - confianza)  # cuantil (negativo)
var_parametrico = -(mu + z*sigma) * valor_cartera

# --- 2. VaR HISTÓRICO (distribución empírica) ---
percentil = np.percentile(retornos, (1-confianza)*100)
var_historico = -percentil * valor_cartera

# --- 3. VaR MONTE CARLO (simulación) ---
np.random.seed(42)
n_sim = 100_000
sim_retornos = np.random.normal(mu, sigma, n_sim)
percentil_mc = np.percentile(sim_retornos, (1-confianza)*100)
var_montecarlo = -percentil_mc * valor_cartera

print(f"=== VaR DIARIO AL {confianza:.0%} (cartera de {valor_cartera:,}€) ===")
print(f"VaR Paramétrico:  {var_parametrico:>12,.0f}€")
print(f"VaR Histórico:    {var_historico:>12,.0f}€")
print(f"VaR Monte Carlo:  {var_montecarlo:>12,.0f}€")
print(f"\nEl VaR histórico suele ser mayor: captura las colas pesadas reales")
print(f"que el paramétrico (normal) y el Monte Carlo (normal) subestiman.")

Interpretación

  • Paramétrico y Monte Carlo (ambos normales): dan resultados similares porque asumen la misma distribución
  • Histórico: suele dar un VaR mayor porque captura las caídas extremas reales que la normal subestima (Módulo 2)
  • Lección: la elección del método importa. Asumir normalidad (paramétrico) puede dar una falsa sensación de seguridad, como ocurrió en 2008

🧪 Caso Práctico 4: VaR vs. CVaR — La Diferencia Crítica

Objetivo

Calcular el CVaR y demostrar por qué supera al VaR para riesgo de cola.

Código

import numpy as np
import matplotlib.pyplot as plt

valor_cartera = 1_000_000
confianza = 0.95
retornos = datos["Close"].pct_change().dropna()

# VaR histórico (umbral)
percentil = np.percentile(retornos, (1-confianza)*100)
var_hist = -percentil * valor_cartera

# CVaR: pérdida MEDIA de los escenarios PEORES que el VaR
cola = retornos[retornos <= percentil]
cvar = -cola.mean() * valor_cartera

print(f"=== VaR vs CVaR al {confianza:.0%} ===")
print(f"VaR (umbral de pérdida):        {var_hist:>12,.0f}€")
print(f"CVaR (pérdida media en la cola): {cvar:>12,.0f}€")
print(f"\nEl CVaR ({cvar:,.0f}€) es mayor que el VaR ({var_hist:,.0f}€)")
print(f"porque promedia TODAS las pérdidas en la cola, no solo el umbral.")

# Visualizar la distribución con VaR y CVaR marcados
plt.figure(figsize=(12, 6))
plt.hist(retornos*100, bins=100, alpha=0.6, color="steelblue", edgecolor="none")
plt.axvline(percentil*100, color="orange", linestyle="--", linewidth=2,
            label=f"VaR 95% ({-var_hist:,.0f}€)")
plt.axvline(cola.mean()*100, color="red", linestyle="--", linewidth=2,
            label=f"CVaR 95% ({-cvar:,.0f}€)")
plt.title("Distribución de retornos: VaR vs CVaR")
plt.xlabel("Retorno diario (%)")
plt.ylabel("Frecuencia")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Interpretación

Qué muestra:

  • El VaR marca el umbral (percentil 5): la pérdida que solo se supera el 5% de las veces
  • El CVaR está más a la izquierda: es la pérdida media CUANDO se supera el VaR
  • El CVaR siempre es ≥ VaR porque promedia toda la cola

Lección clave: dos carteras pueden tener el mismo VaR pero CVaR muy distintos. El CVaR responde “¿cuánto pierdo realmente cuando las cosas van mal?”, que es la pregunta que de verdad importa para sobrevivir.


🧪 Caso Práctico 5: Comparar Estrategias con Todas las Métricas

Objetivo

Construir una función que calcule todas las métricas y comparar varios activos.

Código

import yfinance as yf
import numpy as np
import pandas as pd

def metricas_completas(retornos, rf_anual=0.03, dias=252):
    """Calcula todas las métricas de riesgo y performance."""
    rf_d = rf_anual / dias
    exceso = retornos - rf_d

    # Performance
    ret_anual = retornos.mean() * dias
    vol_anual = retornos.std() * np.sqrt(dias)
    sharpe = (exceso.mean() / retornos.std()) * np.sqrt(dias)

    # Sortino
    downside = retornos[retornos < 0].std()
    sortino = (exceso.mean() / downside) * np.sqrt(dias)

    # Drawdown
    acum = (1 + retornos).cumprod()
    dd = (acum - acum.cummax()) / acum.cummax()
    max_dd = dd.min()
    calmar = ret_anual / abs(max_dd)

    # VaR y CVaR (95%, diario)
    p5 = np.percentile(retornos, 5)
    var95 = -p5
    cvar95 = -retornos[retornos <= p5].mean()

    return {
        "Retorno anual": f"{ret_anual:.1%}",
        "Volatilidad": f"{vol_anual:.1%}",
        "Sharpe": f"{sharpe:.2f}",
        "Sortino": f"{sortino:.2f}",
        "Calmar": f"{calmar:.2f}",
        "Max Drawdown": f"{max_dd:.1%}",
        "VaR 95%": f"{var95:.2%}",
        "CVaR 95%": f"{cvar95:.2%}"
    }

# Comparar varios activos
tickers = ["SPY", "TLT", "GLD", "QQQ"]  # acciones, bonos, oro, tech
datos = yf.download(tickers, start="2018-01-01", end="2024-01-01")["Close"]
retornos = datos.pct_change().dropna()

tabla = pd.DataFrame({t: metricas_completas(retornos[t]) for t in tickers})
print("=== COMPARACIÓN DE MÉTRICAS ===")
print(tabla)

Interpretación

Qué observas (resultados típicos):

  • QQQ (tech): mayor retorno pero también mayor volatilidad y drawdown
  • TLT (bonos): menor retorno, pero a menudo mejor Sharpe en ciertos periodos por su baja volatilidad
  • GLD (oro): comportamiento descorrelacionado, útil como diversificador
  • SPY: el punto de referencia equilibrado

Lección: ninguna métrica decide por sí sola. Un activo puede ganar en retorno pero perder en Sharpe y drawdown. La decisión depende de tus objetivos y tolerancia al riesgo. Esta tabla es exactamente el tipo de análisis que hace un quant antes de construir una cartera (Módulo 6).


🧪 Caso Práctico 6: Escalar el VaR en el Tiempo

Objetivo

Calcular el VaR a distintos horizontes y entender la regla de la raíz del tiempo.

Código

import numpy as np
from scipy import stats

valor_cartera = 1_000_000
confianza = 0.99
retornos = datos["SPY"].pct_change().dropna() if "SPY" in datos else datos.pct_change().dropna()

sigma_diaria = retornos.std()
z = stats.norm.ppf(1 - confianza)

print(f"=== VaR PARAMÉTRICO AL {confianza:.0%} A DISTINTOS HORIZONTES ===")
print(f"(cartera de {valor_cartera:,}€, volatilidad diaria {sigma_diaria:.2%})\n")

for dias in [1, 5, 10, 21, 63, 252]:
    # La volatilidad escala con la RAÍZ del tiempo (Módulo 2)
    sigma_horizonte = sigma_diaria * np.sqrt(dias)
    var = -z * sigma_horizonte * valor_cartera
    print(f"VaR a {dias:>3} días: {var:>12,.0f}€")

print("\nNota: el VaR escala con √tiempo, no linealmente,")
print("igual que la volatilidad (ver Módulo 2).")

Interpretación

  • El VaR a 10 días NO es 10 veces el VaR diario, sino √10 ≈ 3.16 veces
  • Esto se debe a que la volatilidad escala con la raíz del tiempo (la varianza es aditiva, la volatilidad es su raíz) — exactamente lo que vimos en el Módulo 2
  • Aplicación regulatoria: los reguladores suelen exigir VaR a 10 días, calculado escalando el VaR diario por √10

🎓 Proyecto del Módulo

Enunciado

Realiza un análisis de riesgo completo de una cartera a tu elección:

  1. Elige 2-3 activos y crea una cartera con pesos (pueden ser iguales)
  2. Calcula los retornos de la cartera combinada
  3. Calcula todas las métricas: Sharpe, Sortino, Calmar, Max Drawdown
  4. Calcula el VaR (los tres métodos) y el CVaR al 95%
  5. Visualiza la curva de drawdown
  6. Escribe un párrafo evaluando el perfil de riesgo de tu cartera

Solución de Referencia (Plantilla)

import yfinance as yf
import numpy as np
import pandas as pd
from scipy import stats

# 1-2. Cartera
mis_tickers = ["SPY", "TLT", "GLD"]
pesos = np.array([0.5, 0.3, 0.2])  # deben sumar 1
datos = yf.download(mis_tickers, start="2019-01-01", end="2024-01-01")["Close"]
ret_activos = datos.pct_change().dropna()
ret_cartera = (ret_activos * pesos).sum(axis=1)  # retorno combinado

# 3. Métricas de performance
DIAS = 252
ret_anual = ret_cartera.mean() * DIAS
vol_anual = ret_cartera.std() * np.sqrt(DIAS)
sharpe = (ret_cartera.mean() - 0.03/DIAS) / ret_cartera.std() * np.sqrt(DIAS)
acum = (1 + ret_cartera).cumprod()
dd = (acum - acum.cummax()) / acum.cummax()
max_dd = dd.min()

print(f"Retorno anual: {ret_anual:.2%}")
print(f"Volatilidad:   {vol_anual:.2%}")
print(f"Sharpe:        {sharpe:.2f}")
print(f"Max Drawdown:  {max_dd:.2%}")
print(f"Calmar:        {ret_anual/abs(max_dd):.2f}")

# 4. VaR y CVaR al 95%
p5 = np.percentile(ret_cartera, 5)
var95 = -p5 * 1_000_000
cvar95 = -ret_cartera[ret_cartera <= p5].mean() * 1_000_000
print(f"\nVaR 95% (por 1M€):  {var95:,.0f}€")
print(f"CVaR 95% (por 1M€): {cvar95:,.0f}€")

# 5. Gráfico de drawdown
import matplotlib.pyplot as plt
plt.figure(figsize=(12,4))
plt.fill_between(dd.index, dd*100, 0, color="crimson", alpha=0.4)
plt.title("Drawdown de la cartera")
plt.ylabel("%")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

# 6. Tu evaluación aquí

Criterios de Evaluación

  • Construcción de cartera y retornos: correcta (20%)
  • Métricas de performance: bien calculadas (25%)
  • VaR y CVaR: correctos (25%)
  • Visualización del drawdown: clara (15%)
  • Evaluación razonada del riesgo: análisis crítico (15%)

📝 Resumen del Módulo Práctico

Has aprendido a:

✓ Calcular Sharpe, Sortino y Calmar sobre datos reales ✓ Visualizar la curva de drawdown e identificar las peores rachas ✓ Calcular el VaR por los tres métodos (paramétrico, histórico, Monte Carlo) ✓ Calcular el CVaR y entender por qué supera al VaR ✓ Comparar múltiples activos con todas las métricas a la vez ✓ Escalar el VaR a distintos horizontes con la regla de √tiempo

“Cualquiera puede calcular un retorno. Lo que distingue a un quant es la obsesión por el denominador: el riesgo. Domina estas métricas y nunca te dejarás seducir por un retorno alto sin preguntar qué riesgo lo produjo.”

En el Módulo 6 usaremos estas métricas para construir carteras óptimas: optimización mean-variance, risk parity y modelos de factores. Por fin uniremos todo lo aprendido para decidir cómo asignar capital de forma inteligente.


Fin de los Casos Prácticos del Módulo 5