Módulo 3: Casos Prácticos Resueltos
Estadística para Datos Financieros — Laboratorio en Python
📋 Introducción
Estos ejercicios aplican la estadística del Módulo 3 a datos de mercado reales. Aprenderás a comprobar estacionariedad, estimar alpha y beta, modelar volatilidad con GARCH y detectar pares cointegrados. Ejecuta cada bloque y experimenta.
Requisitos Previos
pip install numpy pandas matplotlib scipy yfinance statsmodels arch
Nota:
statsmodelsyarchson las librerías clave de este módulo. Asegúrate de instalarlas.
🧪 Caso Práctico 1: Comprobar Estacionariedad con el Test ADF
Objetivo
Demostrar empíricamente que los precios NO son estacionarios pero los retornos SÍ.
Código
import yfinance as yf
import numpy as np
from statsmodels.tsa.stattools import adfuller
# Descargar datos
datos = yf.download("SPY", start="2018-01-01", end="2024-01-01")
precios = datos["Close"].dropna()
retornos = np.log(precios / precios.shift(1)).dropna()
def test_adf(serie, nombre):
resultado = adfuller(serie)
estadistico, p_valor = resultado[0], resultado[1]
print(f"\n=== Test ADF: {nombre} ===")
print(f"Estadístico ADF: {estadistico:.4f}")
print(f"p-valor: {p_valor:.4f}")
if p_valor < 0.05:
print("→ ESTACIONARIA (rechazamos H₀)")
else:
print("→ NO estacionaria (no rechazamos H₀)")
# Aplicar el test a ambas series
test_adf(precios, "Precios SPY")
test_adf(retornos, "Retornos SPY")
Interpretación
Resultado esperado:
- Precios: p-valor alto (> 0.05) → no estacionarios (tienen tendencia)
- Retornos: p-valor muy bajo (< 0.05) → estacionarios
Lección: este es el primer paso de casi cualquier análisis de series temporales. Antes de aplicar cualquier modelo, comprueba la estacionariedad. La transformación precio → retorno (una diferenciación logarítmica) estacionariza la serie.
🧪 Caso Práctico 2: Autocorrelación — Retornos vs. Volatilidad
Objetivo
Comprobar que los retornos tienen poca autocorrelación pero su volatilidad (retornos al cuadrado) sí tiene memoria.
Código
import matplotlib.pyplot as plt
from statsmodels.graphics.tsaplots import plot_acf
fig, axes = plt.subplots(2, 1, figsize=(12, 8))
# ACF de los retornos
plot_acf(retornos, lags=30, ax=axes[0])
axes[0].set_title("ACF de los retornos (esperamos ≈ 0: casi impredecibles)")
# ACF de los retornos AL CUADRADO (proxy de volatilidad)
plot_acf(retornos**2, lags=30, ax=axes[1])
axes[1].set_title("ACF de los retornos² (esperamos autocorrelación: clustering de volatilidad)")
plt.tight_layout()
plt.show()
# Cuantificar
from statsmodels.stats.diagnostic import acorr_ljungbox
lb_ret = acorr_ljungbox(retornos, lags=[10], return_df=True)
lb_vol = acorr_ljungbox(retornos**2, lags=[10], return_df=True)
print("Test Ljung-Box (p-valor bajo = hay autocorrelación):")
print(f" Retornos: p = {lb_ret['lb_pvalue'].values[0]:.4f}")
print(f" Retornos²: p = {lb_vol['lb_pvalue'].values[0]:.4f}")
Interpretación
Qué observas:
- ACF de retornos: las barras se quedan dentro de las bandas de confianza → autocorrelación cercana a cero. Los retornos son casi impredecibles (eficiencia de mercado).
- ACF de retornos²: muchas barras salen de las bandas → fuerte autocorrelación. Esto es el clustering de volatilidad.
Lección clave del módulo: aunque no puedes predecir la dirección del retorno, sí puedes predecir parcialmente su magnitud (volatilidad). Esta asimetría es la base de GARCH y de gran parte de la gestión de riesgo.
🧪 Caso Práctico 3: Estimar Alpha y Beta con Regresión
Objetivo
Calcular el alpha y beta de una acción frente al mercado e interpretar los resultados.
Código
import yfinance as yf
import numpy as np
import statsmodels.api as sm
# Descargar la acción y el mercado
tickers = ["AAPL", "SPY"] # AAPL = activo, SPY = mercado
datos = yf.download(tickers, start="2020-01-01", end="2024-01-01")["Close"]
retornos = np.log(datos / datos.shift(1)).dropna()
# Variable dependiente (Y) e independiente (X)
Y = retornos["AAPL"]
X = retornos["SPY"]
X = sm.add_constant(X) # añade el término de intersección (alpha)
# Ajustar la regresión por mínimos cuadrados
modelo = sm.OLS(Y, X).fit()
# Extraer resultados
alpha = modelo.params["const"]
beta = modelo.params["SPY"]
r2 = modelo.rsquared
p_alpha = modelo.pvalues["const"]
print("=== MODELO DE MERCADO: AAPL vs SPY ===")
print(f"Alpha (diario): {alpha:.5f} ({alpha*252:.2%} anualizado)")
print(f"Beta: {beta:.3f}")
print(f"R²: {r2:.3f}")
print(f"p-valor del alpha: {p_alpha:.3f}")
print(f"\n¿Alpha significativo? {'SÍ (p<0.05)' if p_alpha < 0.05 else 'NO (p>=0.05)'}")
print(modelo.summary())
Interpretación
Cómo leer los resultados:
- Beta: si es >1, AAPL amplifica los movimientos del mercado (típico de tech)
- Alpha: el retorno extra. CRÍTICO: mira su p-valor. Si p ≥ 0.05, el alpha NO es estadísticamente distinto de cero, aunque parezca positivo
- R²: qué proporción de los movimientos de AAPL explica el mercado; el resto es riesgo específico
Lección: no te emociones con un alpha positivo. La mayoría de las veces no es estadísticamente significativo, o desaparece al añadir más factores de riesgo (Módulo 6).
🧪 Caso Práctico 4: Modelar Volatilidad con GARCH
Objetivo
Ajustar un modelo GARCH(1,1) y visualizar cómo captura el clustering de volatilidad.
Código
import yfinance as yf
import numpy as np
import matplotlib.pyplot as plt
from arch import arch_model
# Datos (retornos en %, GARCH funciona mejor escalado)
datos = yf.download("SPY", start="2018-01-01", end="2024-01-01")
retornos = 100 * np.log(datos["Close"] / datos["Close"].shift(1)).dropna()
# Ajustar GARCH(1,1)
modelo = arch_model(retornos, vol="Garch", p=1, q=1, dist="t")
resultado = modelo.fit(disp="off")
print(resultado.summary())
# Extraer la volatilidad condicional estimada
vol_condicional = resultado.conditional_volatility
# Visualizar
fig, axes = plt.subplots(2, 1, figsize=(13, 8), sharex=True)
axes[0].plot(retornos.index, retornos, linewidth=0.6, color="gray")
axes[0].set_title("Retornos diarios de SPY (%)")
axes[0].grid(True, alpha=0.3)
axes[1].plot(vol_condicional.index, vol_condicional, color="crimson", linewidth=1)
axes[1].set_title("Volatilidad condicional estimada por GARCH(1,1)")
axes[1].set_ylabel("Volatilidad (%)")
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Interpretación
Qué observas:
- En el gráfico de retornos verás “racimos” de alta volatilidad (marzo 2020, finales de 2018, etc.)
- El gráfico de volatilidad GARCH sigue esos racimos: sube cuando hay turbulencia y baja cuando hay calma
Lección: GARCH no predice si el mercado subirá o bajará, pero modela con éxito cuánto se moverá. Esta volatilidad estimada alimenta cálculos de VaR más realistas (Módulo 5) y decisiones de dimensionamiento de posiciones.
🧪 Caso Práctico 5: Predecir Volatilidad Futura con GARCH
Objetivo
Usar el modelo GARCH ajustado para pronosticar la volatilidad de los próximos días.
Código
# Pronóstico de volatilidad para los próximos 10 días
pronostico = resultado.forecast(horizon=10)
# La varianza pronosticada (última fila)
varianza_pred = pronostico.variance.values[-1, :]
vol_pred = np.sqrt(varianza_pred) # volatilidad diaria en %
print("=== PRONÓSTICO DE VOLATILIDAD (próximos 10 días) ===")
for dia, v in enumerate(vol_pred, 1):
print(f"Día {dia}: {v:.3f}% diario ({v*np.sqrt(252):.1f}% anualizado)")
# Visualizar
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 5))
plt.plot(range(1, 11), vol_pred, "o-", color="darkblue")
plt.title("Pronóstico de volatilidad diaria (GARCH)")
plt.xlabel("Días en el futuro")
plt.ylabel("Volatilidad esperada (%)")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Interpretación
Qué muestra:
- Si el mercado está tranquilo, GARCH pronostica que la volatilidad seguirá baja (y converge gradualmente a su media de largo plazo)
- Si viene de un shock, pronostica volatilidad elevada que decae poco a poco
Aplicación real: un gestor de riesgo usa estos pronósticos para ajustar el tamaño de las posiciones. Si se espera alta volatilidad, reduce exposición; si se espera calma, puede asumir más. Esto es gestión de riesgo dinámica en acción.
🧪 Caso Práctico 6: Detectar Pares Cointegrados (Pairs Trading)
Objetivo
Buscar pares de activos cointegrados, base de una estrategia de pairs trading.
Código
import yfinance as yf
import numpy as np
import pandas as pd
from statsmodels.tsa.stattools import coint
import matplotlib.pyplot as plt
# Candidatos del mismo sector (bancos)
tickers = ["JPM", "BAC", "WFC", "C", "GS"]
datos = yf.download(tickers, start="2019-01-01", end="2024-01-01")["Close"].dropna()
# Probar cointegración entre todos los pares
print("=== TEST DE COINTEGRACIÓN (p-valor bajo = cointegrados) ===")
mejores_pares = []
for i in range(len(tickers)):
for j in range(i+1, len(tickers)):
a, b = tickers[i], tickers[j]
score, p_valor, _ = coint(datos[a], datos[b])
if p_valor < 0.05:
mejores_pares.append((a, b, p_valor))
print(f"{a} - {b}: p = {p_valor:.4f} ✓ COINTEGRADOS")
else:
print(f"{a} - {b}: p = {p_valor:.4f}")
# Visualizar el spread del primer par cointegrado encontrado
if mejores_pares:
a, b, _ = mejores_pares[0]
spread = datos[a] - datos[b]
media = spread.mean()
std = spread.std()
plt.figure(figsize=(13, 5))
plt.plot(spread.index, spread, color="purple", linewidth=1, label="Spread")
plt.axhline(media, color="black", linestyle="--", label="Media")
plt.axhline(media + 2*std, color="red", linestyle=":", label="+2σ (vender spread)")
plt.axhline(media - 2*std, color="green", linestyle=":", label="-2σ (comprar spread)")
plt.title(f"Spread cointegrado: {a} - {b}")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
else:
print("\nNo se encontraron pares cointegrados en este periodo.")
Interpretación
Qué muestra:
- El test de cointegración identifica pares cuyos precios se mueven juntos a largo plazo
- El gráfico del spread muestra las bandas de ±2σ: cuando el spread toca el borde superior, se “vende” (apostando a que bajará); cuando toca el inferior, se “compra”
Lección y advertencias:
- La cointegración encontrada en datos pasados puede romperse en el futuro
- Cuidado con el multiple testing: probamos 10 pares; al 5% de significancia, alguno podría parecer cointegrado por azar
- Antes de operar: validar out-of-sample, considerar costos (se operan dos activos) y vigilar cambios fundamentales en las empresas
🎓 Proyecto del Módulo
Enunciado
Realiza un análisis estadístico completo de un activo a tu elección:
- Descarga 5 años de datos
- Comprueba la estacionariedad de precios y retornos (test ADF)
- Visualiza la ACF de retornos y de retornos² (clustering)
- Estima alpha y beta frente al mercado (SPY); evalúa la significancia del alpha
- Ajusta un GARCH(1,1) y pronostica la volatilidad de los próximos 5 días
- Escribe un párrafo con tus conclusiones sobre la predictibilidad del activo
Solución de Referencia (Plantilla)
import yfinance as yf
import numpy as np
import statsmodels.api as sm
from statsmodels.tsa.stattools import adfuller
from arch import arch_model
mi_ticker = "MSFT" # cámbialo
# 1. Datos
datos = yf.download([mi_ticker, "SPY"], start="2019-01-01", end="2024-01-01")["Close"].dropna()
ret = np.log(datos / datos.shift(1)).dropna()
# 2. ADF
print("ADF precios:", adfuller(datos[mi_ticker])[1])
print("ADF retornos:", adfuller(ret[mi_ticker])[1])
# 4. Alpha y beta
X = sm.add_constant(ret["SPY"])
modelo = sm.OLS(ret[mi_ticker], X).fit()
print(f"\nAlpha: {modelo.params['const']:.5f} (p={modelo.pvalues['const']:.3f})")
print(f"Beta: {modelo.params['SPY']:.3f}")
print(f"R²: {modelo.rsquared:.3f}")
# 5. GARCH
ret_pct = 100 * ret[mi_ticker]
garch = arch_model(ret_pct, vol="Garch", p=1, q=1).fit(disp="off")
pred = garch.forecast(horizon=5)
vol5 = np.sqrt(pred.variance.values[-1, :])
print(f"\nVolatilidad pronosticada (5 días): {vol5.round(3)}")
# 6. Tu interpretación aquí
Criterios de Evaluación
- Código funcional: se ejecuta sin errores (25%)
- Tests e interpretación de estacionariedad: correctos (20%)
- Regresión con evaluación de significancia: bien hecha (25%)
- GARCH y pronóstico: correctos (15%)
- Conclusión razonada: análisis de predictibilidad (15%)
📝 Resumen del Módulo Práctico
Has aprendido a:
✓ Comprobar estacionariedad con el test ADF (statsmodels)
✓ Visualizar autocorrelación y detectar el clustering de volatilidad
✓ Estimar alpha y beta con regresión OLS y evaluar su significancia
✓ Modelar la volatilidad con GARCH(1,1) (arch)
✓ Pronosticar volatilidad futura
✓ Detectar pares cointegrados para pairs trading
“Los datos siempre te darán un número. La estadística te dice si ese número significa algo. Aprende a distinguir entre los dos, y estarás por delante de la mayoría.”
En el Módulo 4 daremos el salto a la valoración de derivados: el modelo Black-Scholes, las griegas y la volatilidad implícita, donde las matemáticas del Módulo 2 y la estadística de este módulo se unen para poner precio al riesgo.
Fin de los Casos Prácticos del Módulo 3