Módulo 1: Casos Prácticos Resueltos
¿Qué es un Quant? — Laboratorio en Python
📋 Introducción
Este documento contiene los ejercicios prácticos resueltos del Módulo 1. A diferencia del curso teórico, aquí escribimos código real que puedes ejecutar en tu propio ordenador. El objetivo es que desde el primer módulo toques datos de mercado de verdad.
Requisitos Previos
# Instala las librerías necesarias (en tu terminal)
pip install pandas numpy matplotlib yfinance scipy
Cómo Usar Este Documento
Cada caso incluye: el enunciado, el código comentado y la interpretación de resultados. Copia el código en un notebook Jupyter o un script .py y ejecútalo. No te limites a leer: ejecuta y experimenta.
🧪 Caso Práctico 1: Tu Primer Contacto con Datos Reales
Objetivo
Descargar datos históricos de un activo, visualizarlos y entender su estructura.
Código
import yfinance as yf
import pandas as pd
import matplotlib.pyplot as plt
# Descargar 5 años de datos del ETF SPY (réplica del S&P 500)
spy = yf.download("SPY", start="2019-01-01", end="2024-01-01")
# Inspeccionar la estructura de los datos
print("Forma de los datos:", spy.shape)
print("\nPrimeras filas:")
print(spy.head())
print("\nColumnas disponibles:", list(spy.columns))
# Graficar el precio de cierre ajustado
plt.figure(figsize=(12, 5))
plt.plot(spy.index, spy["Close"], linewidth=1)
plt.title("Precio de cierre de SPY (2019-2024)")
plt.xlabel("Fecha")
plt.ylabel("Precio (USD)")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Interpretación
- Cada fila es un día de trading (los fines de semana y festivos no aparecen)
- Las columnas típicas son: Open, High, Low, Close, Volume
- Lección clave: los datos financieros son series temporales irregulares. Verás caídas notables (COVID en marzo 2020) y recuperaciones.
🧪 Caso Práctico 2: De Precios a Retornos
Objetivo
Entender por qué los quants trabajan con retornos, no con precios, y la diferencia entre retornos simples y logarítmicos.
Código
import numpy as np
# Retornos simples: (precio_hoy - precio_ayer) / precio_ayer
spy["retorno_simple"] = spy["Close"].pct_change()
# Retornos logarítmicos: ln(precio_hoy / precio_ayer)
spy["retorno_log"] = np.log(spy["Close"] / spy["Close"].shift(1))
# Comparar ambos
print("Estadísticas de retornos diarios:")
print(spy[["retorno_simple", "retorno_log"]].describe())
# Visualizar la distribución de retornos
plt.figure(figsize=(12, 5))
plt.hist(spy["retorno_log"].dropna(), bins=100, alpha=0.7, color="steelblue")
plt.title("Distribución de retornos logarítmicos diarios de SPY")
plt.xlabel("Retorno log diario")
plt.ylabel("Frecuencia")
plt.axvline(0, color="red", linestyle="--", alpha=0.5)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Interpretación
¿Por qué retornos y no precios?
- Los precios no son estacionarios (tienen tendencia); los retornos sí lo son aproximadamente
- Los retornos son comparables entre activos de distinto precio
- Los retornos logarítmicos son aditivos en el tiempo: el retorno log de un periodo es la suma de los retornos log diarios
Observación sobre la distribución:
- Parece una campana, pero tiene colas más pesadas que una normal
- Esto significa que los eventos extremos (grandes caídas/subidas) son más frecuentes de lo que predeciría una distribución normal
- Esta es una de las razones por las que LTCM fracasó: subestimaron las colas
🧪 Caso Práctico 3: Calcular Métricas Básicas de Riesgo
Objetivo
Calcular las métricas fundamentales que todo quant usa: retorno anualizado, volatilidad y Sharpe Ratio.
Código
# Retornos log diarios (limpiando NaN)
retornos = spy["retorno_log"].dropna()
# Número de días de trading al año
DIAS_ANIO = 252
# 1. Retorno medio anualizado
retorno_medio_diario = retornos.mean()
retorno_anual = retorno_medio_diario * DIAS_ANIO
print(f"Retorno anualizado: {retorno_anual:.2%}")
# 2. Volatilidad anualizada
vol_diaria = retornos.std()
vol_anual = vol_diaria * np.sqrt(DIAS_ANIO)
print(f"Volatilidad anualizada: {vol_anual:.2%}")
# 3. Sharpe Ratio (asumiendo tasa libre de riesgo = 0 por simplicidad)
sharpe = retorno_anual / vol_anual
print(f"Sharpe Ratio: {sharpe:.2f}")
# 4. Máximo drawdown
precio_acumulado = (1 + spy["retorno_simple"].dropna()).cumprod()
maximo_movil = precio_acumulado.cummax()
drawdown = (precio_acumulado - maximo_movil) / maximo_movil
max_drawdown = drawdown.min()
print(f"Máximo drawdown: {max_drawdown:.2%}")
Interpretación
Resultados esperados aproximados (variarán según fechas):
- Retorno anualizado: ~12-15%
- Volatilidad anualizada: ~18-22%
- Sharpe Ratio: ~0.6-0.8
- Máximo drawdown: ~-34% (el crash de COVID)
Por qué importa la anualización (√252):
- El retorno escala linealmente con el tiempo (×252)
- La volatilidad escala con la raíz cuadrada del tiempo (×√252)
- Esto se debe a que la varianza es aditiva, pero la volatilidad es la raíz de la varianza
- Error común: anualizar la volatilidad multiplicándola por 252 en lugar de √252
🧪 Caso Práctico 4: Detectar Colas Pesadas (Test de Normalidad)
Objetivo
Comprobar empíricamente que los retornos financieros NO siguen una distribución normal.
Código
from scipy import stats
retornos = spy["retorno_log"].dropna()
# Calcular momentos de orden superior
skewness = stats.skew(retornos)
kurtosis = stats.kurtosis(retornos) # exceso de curtosis (normal = 0)
print(f"Asimetría (skewness): {skewness:.3f}")
print(f"Exceso de curtosis: {kurtosis:.3f}")
# Test de Jarque-Bera para normalidad
jb_stat, jb_pvalue = stats.jarque_bera(retornos)
print(f"\nTest Jarque-Bera:")
print(f" Estadístico: {jb_stat:.2f}")
print(f" p-valor: {jb_pvalue:.2e}")
if jb_pvalue < 0.05:
print(" → Rechazamos normalidad: los retornos NO son normales")
else:
print(" → No podemos rechazar normalidad")
# Comparación visual con una normal
plt.figure(figsize=(12, 5))
plt.hist(retornos, bins=100, density=True, alpha=0.6,
color="steelblue", label="Retornos reales")
# Superponer una distribución normal con la misma media y desviación
x = np.linspace(retornos.min(), retornos.max(), 200)
normal = stats.norm.pdf(x, retornos.mean(), retornos.std())
plt.plot(x, normal, "r-", linewidth=2, label="Normal teórica")
plt.title("Retornos reales vs. distribución normal")
plt.xlabel("Retorno log diario")
plt.ylabel("Densidad")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Interpretación
Qué vas a observar:
- Exceso de curtosis positivo (a menudo >3): las colas son más pesadas que en una normal
- p-valor del test ≈ 0: rechazamos contundentemente la normalidad
- Visualmente: el histograma real tiene un pico más alto en el centro y más masa en los extremos
Por qué es crítico esto:
- Muchos modelos clásicos (incluido Black-Scholes) asumen normalidad
- Asumir normalidad subestima el riesgo de eventos extremos
- Un quant competente sabe cuándo esta suposición es peligrosa
🧪 Caso Práctico 5: Correlación vs. Causalidad en la Práctica
Objetivo
Demostrar empíricamente cómo pueden aparecer correlaciones espurias entre activos sin relación.
Código
# Descargar varios activos sin relación obvia
tickers = ["SPY", "GLD", "TLT", "BTC-USD"]
datos = yf.download(tickers, start="2020-01-01", end="2024-01-01")["Close"]
# Calcular retornos
retornos_multi = datos.pct_change().dropna()
# Matriz de correlación
correlaciones = retornos_multi.corr()
print("Matriz de correlación de retornos diarios:")
print(correlaciones.round(3))
# Visualizar como mapa de calor
plt.figure(figsize=(8, 6))
plt.imshow(correlaciones, cmap="coolwarm", vmin=-1, vmax=1)
plt.colorbar(label="Correlación")
plt.xticks(range(len(tickers)), correlaciones.columns, rotation=45)
plt.yticks(range(len(tickers)), correlaciones.columns)
# Añadir los valores numéricos
for i in range(len(correlaciones)):
for j in range(len(correlaciones)):
plt.text(j, i, f"{correlaciones.iloc[i, j]:.2f}",
ha="center", va="center", color="black")
plt.title("Mapa de calor de correlaciones")
plt.tight_layout()
plt.show()
Interpretación
Qué vas a observar:
- SPY (acciones) y TLT (bonos) suelen tener correlación negativa o baja → buenos diversificadores
- GLD (oro) actúa como refugio en ciertos periodos
- BTC tiene correlaciones que cambian mucho con el tiempo
Lección sobre causalidad:
- Una correlación alta entre dos activos NO significa que uno cause el otro
- Las correlaciones cambian con el tiempo (especialmente en crisis, como vimos en el caso de 2008)
- Ejercicio mental: si encontraras una correlación de 0.8 entre dos activos aparentemente no relacionados, ¿buscarías una razón económica antes de operar? (Deberías.)
🧪 Caso Práctico 6: Simulación de “Suerte” vs. “Habilidad”
Objetivo
Demostrar por qué un buen backtest puede ser pura suerte. Generamos estrategias aleatorias y vemos cuántas “parecen” buenas.
Código
import numpy as np
np.random.seed(42)
# Simular 1000 estrategias completamente aleatorias
n_estrategias = 1000
n_dias = 252 * 3 # 3 años de trading
mejores_sharpes = []
for i in range(n_estrategias):
# Retornos diarios completamente aleatorios (media 0, sin edge real)
retornos_aleatorios = np.random.normal(0, 0.01, n_dias)
# Calcular el Sharpe de esta estrategia "aleatoria"
sharpe = (retornos_aleatorios.mean() / retornos_aleatorios.std()) * np.sqrt(252)
mejores_sharpes.append(sharpe)
mejores_sharpes = np.array(mejores_sharpes)
# ¿Cuántas estrategias SIN edge real parecen buenas?
print(f"Estrategias con Sharpe > 0.5: {(mejores_sharpes > 0.5).sum()}")
print(f"Estrategias con Sharpe > 1.0: {(mejores_sharpes > 1.0).sum()}")
print(f"Mejor Sharpe encontrado (puro azar): {mejores_sharpes.max():.2f}")
# Visualizar
plt.figure(figsize=(12, 5))
plt.hist(mejores_sharpes, bins=50, alpha=0.7, color="coral")
plt.axvline(mejores_sharpes.max(), color="red", linestyle="--",
label=f"Mejor por azar: {mejores_sharpes.max():.2f}")
plt.title("Sharpe Ratios de 1000 estrategias SIN edge real (puro azar)")
plt.xlabel("Sharpe Ratio")
plt.ylabel("Número de estrategias")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Interpretación
Resultado clave:
- Aunque NINGUNA estrategia tiene edge real (son ruido puro), algunas tendrán Sharpes aparentemente buenos
- La mejor por puro azar puede superar 0.8-1.0
- Esto es multiple testing en acción
La lección más importante del módulo:
- Si pruebas suficientes estrategias, siempre encontrarás alguna que parece buena por azar
- Por eso el quant serio:
- Parte de una hipótesis económica antes de mirar los datos
- Corrige por el número de pruebas realizadas
- Valida out-of-sample (en datos no usados para diseñar la estrategia)
- Un backtest impresionante sin estos controles no vale nada
🎓 Proyecto del Módulo
Enunciado
Aplica todo lo aprendido en un mini-análisis:
- Elige dos activos que te interesen (acciones, ETFs, cripto)
- Descarga 3 años de datos
- Calcula para cada uno: retorno anualizado, volatilidad y Sharpe
- Calcula la correlación entre ambos
- Comprueba si sus retornos son normales (test Jarque-Bera)
- Escribe un párrafo: ¿formarían una buena pareja diversificadora? ¿Por qué?
Solución de Referencia (Plantilla)
import yfinance as yf
import numpy as np
from scipy import stats
# 1-2. Elige y descarga tus activos
mis_tickers = ["AAPL", "MSFT"] # cámbialos por los tuyos
datos = yf.download(mis_tickers, start="2021-01-01", end="2024-01-01")["Close"]
retornos = np.log(datos / datos.shift(1)).dropna()
# 3. Métricas por activo
for ticker in mis_tickers:
r = retornos[ticker]
ret_anual = r.mean() * 252
vol_anual = r.std() * np.sqrt(252)
sharpe = ret_anual / vol_anual
print(f"\n{ticker}:")
print(f" Retorno anual: {ret_anual:.2%}")
print(f" Volatilidad anual: {vol_anual:.2%}")
print(f" Sharpe: {sharpe:.2f}")
# 4. Correlación
corr = retornos[mis_tickers[0]].corr(retornos[mis_tickers[1]])
print(f"\nCorrelación entre {mis_tickers[0]} y {mis_tickers[1]}: {corr:.3f}")
# 5. Normalidad
for ticker in mis_tickers:
_, pval = stats.jarque_bera(retornos[ticker])
normal = "NO normal" if pval < 0.05 else "posiblemente normal"
print(f"{ticker}: {normal} (p-valor: {pval:.2e})")
# 6. Tu análisis va aquí (en comentarios o markdown)
Criterios de Evaluación
- Código funcional: se ejecuta sin errores (30%)
- Cálculos correctos: métricas bien calculadas (30%)
- Interpretación: análisis razonado de la diversificación (40%)
📝 Resumen del Módulo Práctico
Has aprendido a:
✓ Descargar y visualizar datos reales de mercado con yfinance
✓ Transformar precios en retornos (simples y logarítmicos)
✓ Calcular las métricas fundamentales de riesgo (volatilidad, Sharpe, drawdown)
✓ Comprobar empíricamente que los retornos NO son normales
✓ Analizar correlaciones entre activos
✓ Entender por qué un buen backtest puede ser pura suerte
“El código no miente, pero los datos pueden engañarte. Tu trabajo como quant es saber cuándo confiar en lo que ves.”
En el Módulo 2 profundizaremos en las matemáticas que sustentan todo esto: probabilidad, álgebra lineal y procesos estocásticos.
Fin de los Casos Prácticos del Módulo 1