Etapa 2 de 4 · Fundamentos en Python
Python para finanzas: rentabilidad, volatilidad, Sharpe y drawdown
Tu primer análisis cuantitativo en Python: descarga precios reales, calcula rentabilidades, volatilidad anualizada, ratio de Sharpe y máximo drawdown con pandas, paso a paso.
En este artículo
La mejor manera de empezar en finanzas cuantitativas no es leer teoría: es abrir Python y medir. En este artículo vas a descargar precios reales de bolsa y calcular las cuatro métricas que aparecen en cualquier informe de un fondo: rentabilidad, volatilidad, ratio de Sharpe y máximo drawdown.
Todo el código funciona tal cual en Google Colab, sin instalar nada en tu ordenador.
Preparar el entorno
Necesitas cuatro librerías: numpy y pandas para los cálculos, matplotlib para los gráficos y yfinance para descargar precios de Yahoo Finance. En Colab solo hay que instalar la última:
# En Colab o en una terminal
!pip install yfinanceDescargar precios reales
Vamos a trabajar con tres activos: Inditex y Banco Santander (cotizan en Madrid, de ahí el sufijo .MC) y el SPY, un fondo que replica el S&P 500.
import numpy as np
import pandas as pd
import yfinance as yf
tickers = ["ITX.MC", "SAN.MC", "SPY"]
precios = yf.download(tickers, start="2016-01-01", end="2025-12-31",
auto_adjust=True)["Close"].dropna()
print(precios.tail())Con auto_adjust=True los precios vienen ajustados por dividendos y splits. Es importante: si usas el precio sin ajustar, el día que una acción reparte dividendo parece que cae, y tus métricas salen mal.
Rentabilidades: simples y logarítmicas
Los quants casi nunca trabajan con precios, sino con rentabilidades, porque los precios de activos distintos no son comparables y las rentabilidades sí. Hay dos formas de medirlas:
\[ r_t^{\text{simple}} = \frac{P_t - P_{t-1}}{P_{t-1}}, \qquad r_t^{\log} = \ln\frac{P_t}{P_{t-1}} \]
La simple es la que entiende todo el mundo («ha subido un 2%»). La logarítmica tiene una propiedad muy cómoda: se suma en el tiempo. La rentabilidad logarítmica de un año es la suma de las diarias, cosa que no ocurre con las simples. Para días normales ambas son casi idénticas.
rent = precios.pct_change().dropna() # rentabilidades simples diarias
rent_log = np.log(precios / precios.shift(1)).dropna()
print(rent.describe().round(4))Rentabilidad anualizada
Para comparar activos se habla en términos anuales. La forma honesta de hacerlo es la tasa de crecimiento anual compuesta (CAGR): qué rentabilidad constante cada año te habría llevado del precio inicial al final.
\[ \text{CAGR} = \left(\frac{P_{\text{final}}}{P_{\text{inicial}}}\right)^{1/\text{años}} - 1 \]
años = len(rent) / 252 # ~252 sesiones de bolsa al año
cagr = (precios.iloc[-1] / precios.iloc[0]) ** (1 / años) - 1
print((cagr * 100).round(2))¿Por qué 252? Es el número aproximado de días en que abre la bolsa en un año. Lo usaremos para pasar de métricas diarias a anuales.
Volatilidad anualizada
La volatilidad es la desviación típica de las rentabilidades: cuánto se mueve el activo alrededor de su media. Es la medida de riesgo más básica. Para anualizarla se multiplica por la raíz de 252:
\[ \sigma_{\text{anual}} = \sigma_{\text{diaria}} \cdot \sqrt{252} \]
La raíz sale de que, si las rentabilidades diarias son independientes, las varianzas se suman: la varianza de un año es 252 veces la diaria, y la desviación típica es su raíz.
vol = rent.std() * np.sqrt(252)
print((vol * 100).round(2))Un índice amplio como el S&P 500 suele rondar un 15–20% anual; una acción individual, bastante más, porque no está diversificada.
Ratio de Sharpe
Rentabilidad sola no dice nada: un 20% con un riesgo enorme puede ser peor que un 10% muy estable. El ratio de Sharpe mide la rentabilidad por unidad de riesgo:
\[ \text{Sharpe} = \frac{\mu_{\text{anual}} - r_f}{\sigma_{\text{anual}}} \]
donde \( r_f \) es el tipo sin riesgo (lo que te daría una letra del Tesoro). Como referencia, un Sharpe de 0,5 es normal para un índice a largo plazo, 1 es bueno y por encima de 2 de forma sostenida es excepcional… o un error en el backtest.
rf = 0.02 # tipo sin riesgo anual (ajústalo)
mu = rent.mean() * 252
sharpe = (mu - rf) / vol
print(sharpe.round(2))Cuidado: aquí \( \mu \) es la media aritmética anualizada, que siempre es algo mayor que la CAGR. Es la convención habitual para el Sharpe, pero conviene no mezclar ambas cifras en un mismo informe sin decirlo.
Máximo drawdown: la métrica que de verdad duele
La volatilidad trata igual las subidas y las bajadas. El drawdown mira solo lo que importa a quien invierte: cuánto ha caído desde el último máximo. El máximo drawdown es la peor de esas caídas en todo el periodo.
riqueza = (1 + rent).cumprod() # valor de 1 € invertido
maximo = riqueza.cummax() # máximo alcanzado hasta cada día
drawdown = riqueza / maximo - 1
max_dd = drawdown.min()
print((max_dd * 100).round(2))Es la métrica que explica por qué la gente abandona las estrategias: un −40% significa que necesitas un +67% solo para volver al punto de partida.
Todo junto: tu primer tearsheet
Los fondos resumen una estrategia en una tabla de métricas llamada tearsheet. Esta función hace una versión mínima para cualquier serie de precios:
def metricas(precios, rf=0.02):
r = precios.pct_change().dropna()
años = len(r) / 252
riqueza = (1 + r).cumprod()
vol = r.std() * np.sqrt(252)
return pd.DataFrame({
"CAGR %": ((precios.iloc[-1] / precios.iloc[0]) ** (1 / años) - 1) * 100,
"Volatilidad %": vol * 100,
"Sharpe": (r.mean() * 252 - rf) / vol,
"Máx. drawdown %": (riqueza / riqueza.cummax() - 1).min() * 100,
}).round(2)
print(metricas(precios))Y un gráfico para verlo, porque un número sin gráfico esconde cosas:
import matplotlib.pyplot as plt
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 7), sharex=True)
riqueza.plot(ax=ax1, title="Valor de 1 € invertido")
drawdown.plot(ax=ax2, title="Drawdown")
ax2.yaxis.set_major_formatter(plt.FuncFormatter(lambda y, _: f"{y:.0%}"))
plt.tight_layout()
plt.show()Una cartera en lugar de activos sueltos
Si repartes el dinero a partes iguales entre los tres activos y rebalanceas cada día, la rentabilidad de la cartera es la media ponderada de las rentabilidades:
pesos = np.array([1/3, 1/3, 1/3])
rent_cartera = rent @ pesos
precio_cartera = (1 + rent_cartera).cumprod()
print(metricas(precio_cartera.to_frame("Cartera")))Fíjate en la volatilidad: normalmente sale menor que la media de las volatilidades individuales. Es la diversificación en acción, y es exactamente lo que Markowitz convirtió en un problema de optimización. Lo veremos en optimización de carteras de Markowitz.
Qué has aprendido y qué viene después
- Trabajar con rentabilidades, no con precios, y usar precios ajustados.
- Anualizar: la rentabilidad con CAGR, la volatilidad con \( \sqrt{252} \).
- Medir el riesgo de dos formas complementarias: volatilidad y drawdown.
- Comparar activos con el ratio de Sharpe.
Hasta aquí has mirado al pasado. El siguiente paso es simular el futuro: miles de caminos posibles para un precio con simulación de Monte Carlo en Python.