La pérdida que realmente se siente
La volatilidad trata igual la subida y la bajada, pero el inversor vive con otro número: cuánto está su cuenta por debajo del mejor valor que ha tenido. Eso es el drawdown. Si una cartera subió a 110 y ahora vale 98, está un 10,9 % por debajo de su máximo, diga lo que diga la volatilidad diaria. El drawdown máximo es el agujero más profundo del historial y el tiempo bajo el agua es lo que tardó en salir. Los clientes se van y los gestores de riesgo recortan límites ante los drawdowns, no ante las desviaciones típicas.
import pandas as pd
prices = pd.Series(
[100, 105, 103, 98, 104, 110, 108, 107, 111, 95], dtype=float,
index=pd.bdate_range("2026-03-02", periods=10),
)
peak = prices.cummax() # the highest price so far, day by day
drawdown = prices / peak - 1 # 0 on a new high, negative below it
print(peak.tolist()) # [100.0, 105.0, 105.0, 105.0, 105.0, 110.0, 110.0, 110.0, 111.0, 111.0]
print(drawdown.round(4).tolist()) # [0.0, 0.0, -0.019, -0.0667, -0.0095, 0.0, -0.0182, -0.0273, 0.0, -0.1441]El agujero más profundo
cummax() da el máximo acumulado en una llamada. Dividir el precio entre él y restar 1 da el drawdown de cada día, siempre cero o negativo. El drawdown máximo es simplemente el mínimo de esa serie, e idxmin() dice cuándo ocurrió. Los drawdowns son asimétricos de una forma que importa: una pérdida del 20 % necesita una ganancia del 25 % para recuperarse, 1 / (1 - 0,20) - 1, y una del 50 % necesita el 100 %. Por eso los gestores de riesgo luchan por mantener los drawdowns poco profundos.
print(round(drawdown.min(), 4), drawdown.idxmin().date()) # -0.1441 2026-03-13 111 down to 95
# The climb back is steeper than the fall
for loss in (0.10, 0.20, 0.50):
print(loss, round(1 / (1 - loss) - 1, 3))
# 0.1 0.111
# 0.2 0.25
# 0.5 1.0El tiempo bajo el agua
La profundidad es la mitad de la historia; la duración es la otra mitad. Una estrategia puede perder solo un 8 % y aun así pasar tres años por debajo de su antiguo máximo, lo que pone a prueba la paciencia de cualquier inversor. Un día está bajo el agua mientras su drawdown es negativo, y un nuevo máximo termina el tramo. Para hallar el tramo más largo con pandas, marca los días bajo el agua con una comparación, etiqueta cada racha de valores iguales con (flag != flag.shift()).cumsum() y cuenta dentro de cada racha. Un simple bucle que cuenta y reinicia funciona igual de bien y se lee fácilmente.
underwater = drawdown < 0 # True while below the running peak
run_id = (underwater != underwater.shift()).cumsum() # a new id each time the flag changes
streak = underwater.astype(int).groupby(run_id).cumsum() # 1, 2, 3, ... inside each underwater run
print(streak.tolist()) # [0, 0, 1, 2, 3, 0, 1, 2, 0, 1]
print(int(streak.max())) # 3 the longest wait for a new high, in days# Return divided by the depth of the worst hole: the Calmar ratio
annual_return = 0.12
print(round(annual_return / abs(drawdown.min()), 2)) # 0.83peak = prices.cummax()ydrawdown = prices / peak - 1: cero con un nuevo máximo, negativo por debajo.- El drawdown máximo es
drawdown.min(); una pérdida dexnecesita una ganancia de1 / (1 - x) - 1para recuperarse. - El tiempo bajo el agua es la racha más larga de días con drawdown negativo; el peor drawdown de un backtest es una cota inferior, no una garantía.
A continuación: los generadores de números aleatorios y las semillas, la base de toda simulación.
Lab: qué se califica
- Seguir el máximo alcanzado con
running_peak25 pts - Medir la distancia al máximo con
drawdown_series25 pts - Encontrar la peor caída con
max_drawdown25 pts - Contar la espera más larga por un nuevo máximo con
longest_underwater25 pts