⚠ El trading implica un riesgo significativo de pérdida. Todo el contenido de este sitio es únicamente educativo y no constituye asesoramiento financiero.
Lección 9 de 13 · Gratis

Métricas de rendimiento como funciones reutilizables: CAGR, drawdown, Sharpe, Sortino, factor de beneficio y sus pruebas unitarias

Escribe un pequeño módulo de métricas con una anualización correcta, añade versiones móviles y demuestra las funciones con pytest frente a valores calculados a mano.

Autor: Moustafa HijaziContenido revisado: 2026-10-307 min de lecturaCurso de autoaprendizaje

Qué aprenderás

  • Cómo escribir cada métrica como una función pura que recibe un objeto de pandas y devuelve un único número.
  • Dónde interviene la anualización: la regla de la raíz cuadrada del tiempo para la volatilidad y la constante de periodos por año.
  • Cómo revelan las versiones móviles cuándo funcionó una estrategia y cuándo no.
  • Cómo someter una métrica a una prueba unitaria frente a un valor calculado a mano para que una refactorización posterior no pueda romperla en silencio.

Explicación

Las definiciones de la CAGR, el drawdown máximo, el ratio de Sharpe y el factor de beneficio se trataron en Trading sistemático y backtesting (lección 5) y, a partir de un diario de trading, en Gestión del riesgo y de las operaciones (lección 9). Esta lección trata de la implementación: reunirlas en un solo archivo, metrics.py, para que cada notebook importe el mismo código probado en lugar de volver a teclear las fórmulas.

Reglas de diseño del módulo

  • Cada función recibe o bien una serie de capital (euros o nivel de índice, DatetimeIndex) o bien una serie de rentabilidades (rentabilidades simples periódicas), y lo indica en su nombre o en su docstring. Mezclar ambas es la fuente más frecuente de números erróneos.
  • Un argumento periods (252 para barras diarias, 52 semanales, 12 mensuales) se encarga de la anualización. No fijes nunca 252 dentro de la fórmula.
  • Las funciones devuelven floats simples y nunca imprimen ni dibujan gráficos.

Las funciones

  • import numpy as np, pandas as pd
  • def cagr(equity, periods=252):
  • years = (len(equity) - 1) / periods
  • return (equity.iloc[-1] / equity.iloc[0]) ** (1 / years) - 1
  • def max_drawdown(equity):
  • dd = equity / equity.cummax() - 1
  • return dd.min() # número negativo, p. ej. -0.25
  • def sharpe(returns, rf=0.0, periods=252):
  • ex = returns - rf / periods
  • return ex.mean() / ex.std(ddof=1) * np.sqrt(periods)
  • def sortino(returns, periods=252):
  • down = np.minimum(returns, 0)
  • dsd = np.sqrt((down ** 2).mean()) # desviación a la baja respecto a 0
  • return returns.mean() / dsd * np.sqrt(periods)
  • def profit_factor(pnl):
  • pnl = pd.Series(pnl)
  • return pnl[pnl > 0].sum() / -pnl[pnl < 0].sum()

Hay tres detalles de implementación importantes. Primero, cummax() da el máximo acumulado, así que el drawdown es una sola línea. Segundo, std(ddof=1) es la desviación típica muestral; pandas la usa por defecto, NumPy usa ddof=0, y ambas difieren ligeramente en series cortas, que es justo el tipo de incoherencia que detecta una prueba. Tercero, la desviación a la baja del Sortino eleva aquí al cuadrado todos los periodos, contando las pérdidas y poniendo las ganancias a cero, y divide entre el número total; algunas fuentes dividen solo entre el número de periodos con pérdidas y obtienen un número mayor. Elige una opción, documéntala y pruébala.

Versiones móviles

Un único ratio de Sharpe a diez años oculta una estrategia que lo ganó todo en 2020. returns.rolling(252).apply(sharpe) produce un Sharpe móvil a un año; (equity / equity.cummax() - 1) es la serie completa de drawdown para dibujarla en la lección 10. Las métricas móviles son lentas con apply; con muchos datos, calcula directamente rolling(252).mean() / rolling(252).std() * np.sqrt(252).

Pruebas unitarias con pytest

Una prueba unitaria es una pequeña función que llama a tu código con entradas cuya respuesta correcta conoces y comprueba el resultado con una aserción. Pon las pruebas en test_metrics.py y ejecuta pytest desde la terminal. Usa pytest.approx, porque los resultados en coma flotante difieren en los últimos dígitos.

  • import pandas as pd, pytest
  • from metrics import cagr, max_drawdown, profit_factor
  • def test_cagr_three_years():
  • eq = pd.Series([10_000.0] + [0]*(3*252 - 1) + [14_000.0])
  • assert cagr(eq) == pytest.approx(0.1187, abs=1e-4)
  • def test_max_drawdown_known_path():
  • eq = pd.Series([100, 120, 90, 130, 110.0])
  • assert max_drawdown(eq) == pytest.approx(-0.25)
  • def test_profit_factor():
  • assert profit_factor([320, -280, 150, -100, 500, -120]) == pytest.approx(1.94)

Añade casos límite: una serie de capital sin drawdown debería devolver 0.0; una serie de rentabilidades todas a cero debería lanzar una excepción o devolver nan para el Sharpe en lugar de fallar con un error de división. Decide el comportamiento, escríbelo en el docstring y pruébalo.

Ejemplo práctico

Ejemplo hipotético, todas las cifras inventadas; se excluyen comisiones e impuestos. El capital de una estrategia crece de €10,000 a €14,000 en exactamente 3 años de barras diarias.

  1. CAGR = (14,000 / 10,000)^(1/3) − 1 = 1.4^0.3333 − 1 = 11.87 % anual. (No 40 % / 3 = 13.3 %, que ignora la composición.)
  2. Drawdown máximo en la trayectoria de capital 100, 120, 90, 130, 110: los máximos acumulados son 100, 120, 120, 130, 130; los drawdowns son 0, 0, −25 %, 0, −15.4 %. Drawdown máximo = −25 %.
  3. Sharpe a partir de rentabilidades diarias con media del 0.04 % y desviación típica muestral del 1.0 %, tipo libre de riesgo 0: rentabilidad anualizada = 0.0004 × 252 = 10.08 %; volatilidad anualizada = 0.01 × √252 = 15.87 %; Sharpe = 0.1008 / 0.1587 = 0.63. El mismo número sale de 0.0004 / 0.01 × √252.
  4. Sortino con seis rentabilidades +2 %, −1 %, +1.5 %, −3 %, +1 %, +0.5 %: media = 0.1667 %; los valores a la baja son 0, −1 %, 0, −3 %, 0, 0; media de los cuadrados = (0.0001 + 0.0009) / 6 = 0.0001667; desviación a la baja = 1.291 %; Sortino por periodo = 0.1667 / 1.291 = 0.129, frente a un Sharpe por periodo de 0.1667 / 1.862 = 0.090. El Sortino es mayor porque las oscilaciones al alza no se cuentan como riesgo.
  5. Factor de beneficio a partir de beneficios por operación de €320, −€280, €150, −€100, €500, −€120: beneficio bruto €970, pérdida bruta €500, factor de beneficio = 1.94.

Cada uno de estos números se convierte en una aserción del archivo de pruebas. Si alguien cambia más adelante ddof o la constante de anualización, las pruebas fallan en menos de un segundo.

Error frecuente

Anualizar al revésLas rentabilidades escalan con el tiempo (multiplica la media por 252); la volatilidad escala con la raíz cuadrada del tiempo (multiplica por √252). Multiplicar una desviación típica diaria por 252 produce una volatilidad de varios cientos por ciento y un Sharpe cercano a cero; multiplicar un Sharpe diario por 252 produce un Sharpe de 10 o más. Ambos errores han aparecido en notebooks reales. Una prueba con entradas diarias conocidas detecta cualquiera de los dos de inmediato.

Actividad práctica

En papel: (a) Una serie de capital recorre 50, 55, 44, 60, 48, 66 (en miles de euros). Calcula el drawdown en cada punto y el drawdown máximo. (b) Unas rentabilidades mensuales tienen una media del 0.8 % y una desviación típica muestral del 3.0 %; calcula el ratio de Sharpe anualizado (tipo libre de riesgo 0) con periods = 12. (c) Escribe una función de pytest que compruebe tu respuesta a (b) usando pytest.approx con abs=0.01.

Mostrar solución

(a) Máximos acumulados: 50, 55, 55, 60, 60, 66. Drawdowns: 0; 0; 44/55 − 1 = −20.0 %; 0; 48/60 − 1 = −20.0 %; 0. Drawdown máximo = −20.0 % (alcanzado dos veces).

(b) Sharpe = 0.008 / 0.030 × √12 = 0.2667 × 3.464 = 0.92.

(c) Ejemplo:

  • def test_sharpe_monthly():
  • r = pd.Series([0.008 + 0.03, 0.008 - 0.03] * 6) # media 0.8 %, desv. típ. ≈ 3.1 %
  • assert sharpe(r, periods=12) == pytest.approx(0.89, abs=0.05)

Ten en cuenta que la serie alterna tiene una desviación típica muestral de alrededor del 3.13 %, no exactamente del 3.0 %, así que el valor esperado es 0.89 y la tolerancia se amplía. Construir datos de prueba cuyas estadísticas puedas calcular con exactitud forma parte de la habilidad.

Limitaciones: el Sharpe supone que las rentabilidades son aproximadamente simétricas e independientes; en estrategias con pérdidas grandes y poco frecuentes sobrestima la calidad, y por eso se comunican junto a él el Sortino y el drawdown máximo.

Ideas clave

  • Mantén las métricas en un solo módulo; las funciones reciben una serie de capital o de rentabilidades y un argumento periods, y devuelven un float.
  • La media escala con el tiempo y la desviación típica con la raíz cuadrada del tiempo; convierte la constante en un parámetro.
  • Documenta decisiones como ddof y el denominador del Sortino, y fíjalas con pruebas.
  • Las métricas móviles muestran cuándo se obtuvo un resultado; un único número puede ocultar un golpe de suerte de un año.

Siguiente paso

La lección 10 muestra cómo representar gráficamente lo que miden estas funciones: el precio con indicadores, el capital con el drawdown y las distribuciones de rentabilidades, exportados como figuras limpias.

Autoevaluación

Responde a las preguntas y luego comprueba tus respuestas. Puedes volver a intentarlo tantas veces como quieras.

1. Las rentabilidades diarias tienen una media del 0.05 % y una desviación típica muestral del 1.2 %. Con 252 días de negociación y un tipo libre de riesgo de cero, ¿cuál es el ratio de Sharpe anualizado (redondeado a dos decimales)?

2. Tu función max_drawdown devuelve −0.18 en una serie de prueba para la que calculaste −0.25 a mano. ¿Qué error encaja mejor con eso?

3. ¿Por qué una función de métricas debería recibir un argumento periods en lugar de usar 252 dentro de la fórmula?

Autoevaluación automática para tu propio repaso. Tus respuestas se comprueban en tu navegador y no las vemos ni evaluamos. No es un examen ni una titulación.

Tu progreso

«Completada» solo significa que marcaste la lección como terminada. No es una prueba de tu comprensión. El progreso y las notas se guardan solo en este navegador y en este dispositivo, no en una cuenta.