Lección 0.7.1 · 50 min
Rentabilidades simples y logarítmicas, interés compuesto y estadística descriptiva
Casi todas las cifras de un backtest salen de una serie de rentabilidades: la media, la volatilidad, la rentabilidad anual, el peor día. Esta lección fija cómo se calculan, cuál se suma y cuál se multiplica, cómo se anualizan y qué dicen la asimetría y la curtosis de una distribución que no es normal.
- Módulo
- 0.7 · Matemáticas y estadística mínimas
- Duración
- 50 min
Lecciones del nivel 0
0.1 · Bienvenida y expectativas
0.2 · Entorno de trabajo moderno
- 0.2.1 Instalación de Python con uv: versiones y ciclo de soporte
- 0.2.2 Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles
- 0.2.3 Editores (VS Code, Cursor, PyCharm) y notebooks (Jupyter, JupyterLab, marimo)
- 0.2.4 Terminal básica, rutas, variables de entorno y ficheros .env
- 0.2.5 Git y GitHub: commits, ramas, pull requests y .gitignore
- 0.2.6 Introducción a los asistentes de código con IA: prompts, revisión y tests
0.3 · Fundamentos del lenguaje
- 0.3.1 Variables, tipos y operadores; coma flotante y decimal para precios y ticks
- 0.3.2 Cadenas, f-strings y formato de números y fechas
- 0.3.3 Listas, tuplas, diccionarios, conjuntos y comprensiones
- 0.3.4 Control de flujo: if, elif, else, for, while y match-case
- 0.3.5 Funciones: argumentos, *args y **kwargs, lambdas y ámbito
- 0.3.6 Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15
0.4 · Python intermedio
- 0.4.1 Programación orientada a objetos: clases, herencia, composición y dataclasses
- 0.4.2 Tipado: type hints, mypy o ty y Pydantic para configuraciones
- 0.4.3 Iteradores, generadores, decoradores y context managers
- 0.4.4 Ficheros (CSV, JSON, YAML, TOML) y fechas con zonas horarias
- 0.4.5 Logging profesional y testing con pytest
- 0.4.6 Concurrencia: threading, multiprocessing, asyncio y free-threading
0.5 · Computación numérica y datos
- 0.5.1 NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios
- 0.5.2 pandas 3.0: Series, DataFrame, índices temporales, Copy-on-Write, tipo str y pd.col()
- 0.5.3 Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles
- 0.5.4 Polars: API lazy frente a eager, expresiones, rendimiento y cuándo elegirlo frente a pandas
- 0.5.5 DuckDB y SQL esencial: SELECT, JOIN, GROUP BY y funciones de ventana sobre Parquet
0.6 · Visualización
0.7 · Matemáticas y estadística mínimas
Del precio a la rentabilidad
Un precio aislado no dice nada: 142,22 puede ser mucho o poco. Lo que se compara entre activos, periodos y estrategias es la rentabilidad, el cambio relativo del precio. Hay dos formas de medirla, y casi todos los errores de cálculo de un backtest principiante vienen de mezclarlas.
La rentabilidad simple es la variación porcentual de toda la vida:
La rentabilidad logarítmica (o continua) es el logaritmo natural del cociente de precios:
En pandas, la primera es pct_change() y la segunda, la diferencia de los logaritmos de los precios. Con los cierres
de SINT, el ETF ficticio del curso:
import numpy as np
import pandas as pd
precios = pd.read_csv("sint_diario.csv", parse_dates=["fecha"],
index_col="fecha")
cierre = precios["cierre"]
rent = pd.DataFrame({
"simple": cierre.pct_change(),
"log": np.log(cierre).diff(),
}).dropna()
rent.head(3).round(5) simple log
fecha
2021-01-05 0.00111 0.00111
2021-01-06 -0.01884 -0.01902
2021-01-07 0.01212 0.01204El primer día desaparece con dropna(): no tiene día anterior. En movimientos diarios de una décima o de un punto
porcentual, las dos medidas difieren en centésimas de punto: −1,884 % frente a −1,902 % el segundo día. Se separan cuando el movimiento es grande:
for R in (-0.5, -0.2, -0.05, -0.01, 0.01, 0.05, 0.2, 1.0):
print(f"simple {R:+7.0%} log {np.log1p(R):+8.2%}")simple -50% log -69.31%
simple -20% log -22.31%
simple -5% log -5.13%
simple -1% log -1.01%
simple +1% log +1.00%
simple +5% log +4.88%
simple +20% log +18.23%
simple +100% log +69.31%np.log1p(R) calcula con más precisión que np.log(1 + R) cuando es muy pequeño. La tabla muestra
la asimetría de la rentabilidad simple: una caída del 50 % es −69,31 % en log, y una subida del 100 % (la que hace
falta para recuperarla, lección 0.1.2) es +69,31 %. En log, perder y recuperar lo perdido pesan lo mismo; en simple,
no.
Cuál se suma y cuál se multiplica
La diferencia práctica entre las dos está en cómo se agregan.
En el tiempo, las simples se componen (se multiplican) y las logarítmicas se suman:
total_precios = cierre.iloc[-1] / cierre.iloc[0] - 1
total_simple = (1 + rent["simple"]).prod() - 1
total_log = np.exp(rent["log"].sum()) - 1
suma_simple = rent["simple"].sum()
print(f"Del primer al último cierre: {total_precios:.4%}")
print(f"Producto de (1 + R): {total_simple:.4%}")
print(f"exp(suma de r) - 1: {total_log:.4%}")
print(f"Suma de R (mal): {suma_simple:.4%}")Del primer al último cierre: 43.4537%
Producto de (1 + R): 43.4537%
exp(suma de r) - 1: 43.4537%
Suma de R (mal): 44.5425%Las tres primeras cifras son la misma, calculada de tres formas. La cuarta, sumar rentabilidades simples, es un error frecuente que aquí infla el resultado en algo más de un punto (44,54 % frente a 43,45 %), y el error crece con la volatilidad y con el número de periodos: ignora que cada día se gana o se pierde sobre un capital distinto.
Entre activos, en un mismo periodo, es al revés: la rentabilidad simple de una cartera es exactamente la media de las simples de sus activos, ponderada por su peso al empezar el periodo. La logarítmica no cumple esa propiedad. Un día con dos posiciones del mismo tamaño, una que sube un 20 % y otra que cae un 20 %:
pesos = np.array([0.5, 0.5])
R_dia = np.array([0.20, -0.20])
print(f"Cartera (simples ponderadas): {pesos @ R_dia:+.4%}")
print(f"Media ponderada de los log: "
f"{pesos @ np.log1p(R_dia):+.4%}")Cartera (simples ponderadas): +0.0000%
Media ponderada de los log: -2.0411%La cartera no gana ni pierde: 0 %, y su rentabilidad logarítmica es . La media ponderada de los
logaritmos da −2,04 %, una pérdida que no existe. El operador @ es el producto escalar de la lección 0.5.1; la
lección 0.7.3 lo usa para carteras de verdad.
| Para… | Usa | Porque |
|---|---|---|
| Encadenar periodos (días a años) | Logarítmicas (suma) o simples (producto) | Las dos sirven; la suma de log es más cómoda |
| Combinar activos de una cartera | Simples | La simple de la cartera es la media ponderada de las simples |
| Informar a una persona | Simples | «Ha ganado un 7 %» se entiende; «0,068 en log», no |
| Modelos estadísticos (lección 0.7.2) | Logarítmicas, a menudo | Suman, y son algo más simétricas |
Interés compuesto y CAGR
El interés compuesto es la regla de las rentabilidades simples aplicada a un capital: cada periodo se gana sobre lo ganado antes. Un capital que rinde una tasa al año durante años termina en :
capital = 10_000
for anios in (10, 20, 30):
print(f"{anios} años: {capital * 1.07 ** anios:>9,.0f} €")10 años: 19,672 €
20 años: 38,697 €
30 años: 76,123 €A los treinta años el capital se ha multiplicado por más de siete, y los diez últimos años aportan más que los veinte primeros. Es el motivo por el que los costes y las comisiones anuales, pequeños cada año, son tan caros a largo plazo (lección 0.1.2).
La frecuencia con que se compone también cuenta. Un 7 % nominal compuesto cada mes rinde algo más que compuesto una vez al año, y el límite es la composición continua, , que es justo la relación entre rentabilidad logarítmica y simple:
tasa = 0.07
for nombre, m in (("anual", 1), ("mensual", 12), ("diaria", 252)):
print(f"{nombre:<9}{(1 + tasa / m) ** m - 1:.4%}")
print(f"{'continua':<9}{np.exp(tasa) - 1:.4%}")anual 7.0000%
mensual 7.2290%
diaria 7.2498%
continua 7.2508%La operación inversa responde a la pregunta habitual sobre un histórico: ¿a qué tasa anual constante habría que crecer para ir del primer precio al último? Es la tasa anual compuesta, CAGR (compound annual growth rate):
anios = (cierre.index[-1] - cierre.index[0]).days / 365.25
cagr = (cierre.iloc[-1] / cierre.iloc[0]) ** (1 / anios) - 1
print(f"Años: {anios:.2f} · CAGR: {cagr:.2%}")
print(f"Sesiones por año: {len(cierre) / anios:.0f}")Años: 4.99 · CAGR: 7.50%
Sesiones por año: 261Los años se cuentan con las fechas: del 4 de enero de 2021 al 31 de diciembre de 2025 hay 4,99 años. La segunda línea muestra por qué no conviene contarlos dividiendo sesiones entre 252: SINT no tiene festivos y cotiza 261 días al año, así que dividir sus 1.303 sesiones entre 252 daría 5,17 años y un CAGR menor del real.
Anualizar: media, volatilidad y el coste de la volatilidad
Las rentabilidades diarias son números pequeños; para comparar estrategias se anualizan. Si las rentabilidades de días distintos son independientes, la media y la varianza crecen con el número de días, y la desviación típica, con su raíz:
La desviación típica anualizada es la volatilidad, la medida de riesgo más usada. Se puede comprobar con datos mensuales, que se anualizan con :
vol_d = rent["simple"].std()
mensual = cierre.resample("ME").last().pct_change().dropna()
vol_m = mensual.std()
print(f"Diaria: {vol_d:.2%} · anualizada {vol_d * np.sqrt(252):.2%}")
print(f"Mensual: {vol_m:.2%} · anualizada {vol_m * np.sqrt(12):.2%}")Diaria: 1.14% · anualizada 18.10%
Mensual: 4.73% · anualizada 16.39%Las dos anualizadas se parecen pero no coinciden. Con 59 meses, la estimación mensual tiene mucho más error que la diaria, con 1.302 días; y si los días no fueran del todo independientes (rachas, volatilidad que se agrupa en episodios), la regla de la raíz dejaría de ser exacta. En la práctica, la volatilidad se estima con datos diarios y se anualiza con , sabiendo que es una aproximación.
La media tiene una trampa más. La media aritmética de las rentabilidades simples, anualizada, no es lo que crece el capital: lo que crece es la media de las logarítmicas. La diferencia es aproximadamente la mitad de la varianza:
media_R = rent["simple"].mean()
media_r = rent["log"].mean()
varianza = rent["simple"].var()
print(f"Media simple ×252: {media_R * 252:.2%}")
print(f"Media log ×252: {media_r * 252:.2%}")
print(f"Diferencia ×252: {(media_R - media_r) * 252:.2%}")
print(f"σ²/2 ×252: {varianza / 2 * 252:.2%}")Media simple ×252: 8.62%
Media log ×252: 6.98%
Diferencia ×252: 1.64%
σ²/2 ×252: 1.64%La aproximación acierta casi al céntimo. Es el coste de la volatilidad (volatility drag): dos estrategias con la misma media aritmética crecen distinto si una es más volátil. Un folleto que anuncia «un 8,6 % de media anual» con la media aritmética de SINT promete más de lo que el capital habría crecido de verdad.
Describir la distribución
Media y volatilidad resumen el centro y la dispersión. Dos estadísticos más dicen cómo es la forma:
- Asimetría (skewness): negativa si la cola de las pérdidas es más larga que la de las ganancias (caídas bruscas y subidas lentas, típico de los índices de acciones); positiva en el caso contrario (típico de las estrategias de tendencia, con muchas pérdidas pequeñas y pocas ganancias grandes). La normal tiene 0.
- Curtosis: mide el peso de las colas. La normal tiene curtosis 3; la curtosis en exceso resta ese 3, y es la que devuelven pandas y, por defecto, SciPy. Un valor positivo indica colas más gruesas que la normal: más días extremos de los que la normal espera.
from scipy import stats
r = rent["simple"]
print(r.describe().round(4).to_string())
print(f"Asimetría pandas {r.skew():.3f} · scipy "
f"{stats.skew(r):.3f}")
print(f"Curtosis exceso pandas {r.kurt():.3f} · scipy "
f"{stats.kurtosis(r):.3f}")count 1302.0000
mean 0.0003
std 0.0114
min -0.0481
25% -0.0068
50% 0.0006
75% 0.0074
max 0.0508
Asimetría pandas -0.015 · scipy -0.015
Curtosis exceso pandas 1.084 · scipy 1.075describe() da el recuento, la media, la desviación típica, el mínimo, los cuartiles y el máximo. La mediana (50 %)
queda algo por encima de la media, y el peor día (−4,81 %) y el mejor (+5,08 %) están a más de cuatro desviaciones
típicas del centro. La asimetría es prácticamente cero: SINT no tiene sesgo hacia las caídas. La curtosis en exceso
de 1,08 dice que las colas son más gruesas que las de la normal.
pandas y SciPy no dan exactamente lo mismo. pandas corrige el sesgo de la muestra (como la desviación típica con
); SciPy, por defecto, no (bias=True). Con 1.302 datos la diferencia está en el segundo decimal; con 60, no.
Al comparar cifras de dos fuentes, mira con qué función se calcularon.
La figura de la cabecera dibuja la distribución completa: las barras son las rentabilidades diarias de SINT; la curva, una normal con la misma media y la misma desviación típica. La barra central sobresale por encima de la curva (más días tranquilos) y en los extremos aparecen barras donde la curva casi no llega. Contados:
fuera = int((r.abs() > 0.035).sum())
normal = stats.norm(loc=r.mean(), scale=r.std())
esperados = len(r) * (normal.cdf(-0.035) + normal.sf(0.035))
print(f"Días con |R| > 3,5 %: {fuera} · normal: {esperados:.1f}")Días con |R| > 3,5 %: 11 · normal: 2.8Casi cuatro veces más días extremos de los que una normal predice. Es la forma de la curtosis que importa en riesgo: un modelo que supone rentabilidades normales subestima la frecuencia de los días malos. La lección 0.7.2 mide esa diferencia con probabilidades y prueba una distribución con colas más gruesas, la t de Student.
Ejercicios
- Comprueba con código el ejemplo de la autoevaluación: un +10 % seguido de un −10 %. Calcula la rentabilidad total con simples (producto) y con logarítmicas (suma). ¿Qué subida hace falta tras el −10 % para volver al punto de partida?
Solución
dias = np.array([0.10, -0.10])
print(f"Producto de simples: {np.prod(1 + dias) - 1:+.2%}")
print(f"Suma de log: {np.log1p(dias).sum():+.4f}")
print(f"exp(suma) - 1: {np.expm1(np.log1p(dias).sum()):+.2%}")
print(f"Subida necesaria: {1 / 0.9 - 1:+.2%}")Producto de simples: -1.00%
Suma de log: -0.0101
exp(suma) - 1: -1.00%
Subida necesaria: +11.11%Las dos vías dan el mismo −1 %: la suma de log es −0,0101, y . Para recuperar un −10 %
hace falta un +11,11 %. np.expm1(x) es , el inverso de np.log1p.
- Calcula la volatilidad anualizada, la asimetría y la curtosis en exceso de las rentabilidades diarias de los cuatro
activos de
cartera_sint.csv. ¿Cuál tiene las colas más gruesas?
Solución
cartera = pd.read_csv("cartera_sint.csv", parse_dates=["fecha"],
index_col="fecha")
rc = cartera.pct_change().dropna()
resumen = pd.DataFrame({
"vol_anual": rc.std() * np.sqrt(252),
"asimetria": rc.skew(),
"curtosis_exceso": rc.kurt(),
})
resumen.round(3) vol_anual asimetria curtosis_exceso
RVAR 0.168 0.013 0.087
RFIJ 0.050 -0.087 -0.197
ORO 0.151 0.048 -0.103
TECH 0.283 0.039 0.147Los métodos de DataFrame calculan cada estadístico por columna, así que cada columna de la tabla sale en una
línea. TECH, el más volátil, tiene la curtosis más alta (0,147), pero las cuatro están cerca de cero: estos activos se
generaron con una volatilidad constante y una distribución normal. SINT, con dos episodios de volatilidad alta, llega
a 1,08. Mezclar periodos tranquilos y periodos agitados es una de las causas de las colas gruesas, y en series de
mercado reales las colas suelen ser más gruesas todavía que en SINT.
- Calcula el CAGR de SINT de cada año natural (del último cierre del año anterior al último del año, y para 2021 desde el primer cierre) y compáralo con la media aritmética anualizada de las rentabilidades diarias de ese año. ¿En qué año es mayor la diferencia y por qué?
Solución
fin = cierre.groupby(cierre.index.year).last()
base = fin.shift(1).fillna(cierre.iloc[0])
anual = pd.DataFrame({
"rent_anual": fin / base - 1,
"media_x252": rent["simple"].groupby(
rent.index.year).mean() * 252,
"vol": rent["simple"].groupby(rent.index.year).std()
* np.sqrt(252),
})
anual["diferencia"] = anual["media_x252"] - anual["rent_anual"]
anual.round(3) rent_anual media_x252 vol diferencia
fecha
2021 0.152 0.150 0.160 -0.002
2022 -0.085 -0.055 0.248 0.030
2023 0.142 0.140 0.149 -0.002
2024 0.143 0.140 0.150 -0.003
2025 0.042 0.056 0.179 0.014Cada año es un año completo, así que la rentabilidad del año es su CAGR. En 2022, el año más volátil (24,8 %), la media aritmética anualizada supera a la rentabilidad real en 3 puntos; en 2025 (17,9 %), en 1,4. Es el coste de la volatilidad, que crece con su cuadrado. En los años tranquilos la diferencia sale casi nula o ligeramente negativa: con 261 sesiones, multiplicar la media diaria por 252 se queda corto, y ese error compensa un coste de la volatilidad que, con un 15 %, es de apenas un punto ().
Respuesta plegada en cada pregunta
Autoevaluación
Una acción sube un 10 % un día y baja un 10 % al siguiente. ¿Dónde queda?
- AIgual que al principio
- BUn 1 % por debajo: 1,10 × 0,90 = 0,99
- CUn 1 % por encima
- DDepende del precio inicial
Ver respuesta
Respuesta B. Un 1 % por debajo: 1,10 × 0,90 = 0,99
Las rentabilidades simples se multiplican, no se suman: (1 + 0,10) × (1 − 0,10) − 1 = −1 %. Con rentabilidades logarítmicas sí se suman, y +10 % y −10 % simples son +9,53 % y −10,54 % en log.
Tienes las rentabilidades diarias de dos activos y una cartera 50/50 rebalanceada a diario. ¿Cómo calculas la rentabilidad de la cartera de un día?
- AMedia de las rentabilidades logarítmicas
- BMedia ponderada de las rentabilidades simples
- CSuma de las rentabilidades logarítmicas
- DProducto de las rentabilidades simples
Ver respuesta
Respuesta B. Media ponderada de las rentabilidades simples
Entre activos, en un mismo periodo, la rentabilidad simple de la cartera es exactamente la media ponderada de las simples. La media ponderada de las logarítmicas no es la logarítmica de la cartera.
La volatilidad diaria de un activo es del 1 %. ¿Cuál es su volatilidad anualizada con la convención habitual?
- A252 %
- BUn 15,9 %, el 1 % multiplicado por la raíz de 252
- CUn 12 %, el 1 % por 12 meses
- DUn 1 %, la volatilidad no se anualiza
Ver respuesta
Respuesta B. Un 15,9 %, el 1 % multiplicado por la raíz de 252
Si las rentabilidades diarias son independientes, la varianza crece con el número de días y la desviación típica con su raíz: 1 % × √252 ≈ 15,9 %.
Una serie de rentabilidades tiene curtosis en exceso de 1,1 según
Series.kurt(). ¿Qué significa?- AQue la distribución es normal
- BQue tiene más días extremos (y más días tranquilos) que una normal con la misma volatilidad
- CQue la media es mayor que la mediana
- DQue hay un error: la curtosis no puede ser positiva
Ver respuesta
Respuesta B. Que tiene más días extremos (y más días tranquilos) que una normal con la misma volatilidad
pandas devuelve la curtosis en exceso: 0 para una normal. Un valor positivo indica colas más gruesas y un centro más alto; en riesgo, más días extremos de los que la normal predice.
Ideas clave
- Rentabilidad simple: el cierre de hoy entre el de ayer, menos 1; logarítmica: el logaritmo natural de ese cociente, ln(1 + R). Para movimientos diarios pequeños casi coinciden; para movimientos grandes, no.
- En el tiempo, las simples se multiplican y las logarítmicas se suman. Entre activos, la simple de la cartera es la media ponderada de las simples.
- El CAGR se calcula con los años transcurridos según el calendario. La media aritmética anualizada lo supera en torno a σ²/2: la volatilidad resta crecimiento.
- La media se anualiza por 252 y la volatilidad por √252, suponiendo días independientes y el calendario de un mercado real.
- Las rentabilidades reales tienen colas más gruesas que la normal: curtosis en exceso positiva y más días extremos de los que la normal espera.