Lección 0.5.3 · 55 min

Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles

Casi todo el research diario con pandas se reduce a ocho operaciones: seleccionar, filtrar, agrupar, combinar, pivotar, cambiar de frecuencia y calcular sobre ventanas. Esta lección las recorre con SINT hasta llegar a su volatilidad móvil y su drawdown, sin usar información del futuro.

Fig. — Volatilidad de SINT en ventanas de 21 sesiones, anualizada (último valor de cada semana), 2021–2025
03/05
Módulo
0.5 · Computación numérica y datos
Duración
55 min
Lecciones del nivel 0
  1. 0.1 · Bienvenida y expectativas

    1. 0.1.1 Trading sistemático, algorítmico y cuantitativo frente al discrecional
    2. 0.1.2 Expectativas realistas: costes, tiempo y riesgo de pérdida
    3. 0.1.3 Estructura del curso, tests de entrada, repositorio personal y aprendizaje con IA
  2. 0.2 · Entorno de trabajo moderno

    1. 0.2.1 Instalación de Python con uv: versiones y ciclo de soporte
    2. 0.2.2 Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles
    3. 0.2.3 Editores (VS Code, Cursor, PyCharm) y notebooks (Jupyter, JupyterLab, marimo)
    4. 0.2.4 Terminal básica, rutas, variables de entorno y ficheros .env
    5. 0.2.5 Git y GitHub: commits, ramas, pull requests y .gitignore
    6. 0.2.6 Introducción a los asistentes de código con IA: prompts, revisión y tests
  3. 0.3 · Fundamentos del lenguaje

    1. 0.3.1 Variables, tipos y operadores; coma flotante y decimal para precios y ticks
    2. 0.3.2 Cadenas, f-strings y formato de números y fechas
    3. 0.3.3 Listas, tuplas, diccionarios, conjuntos y comprensiones
    4. 0.3.4 Control de flujo: if, elif, else, for, while y match-case
    5. 0.3.5 Funciones: argumentos, *args y **kwargs, lambdas y ámbito
    6. 0.3.6 Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15
  4. 0.4 · Python intermedio

    1. 0.4.1 Programación orientada a objetos: clases, herencia, composición y dataclasses
    2. 0.4.2 Tipado: type hints, mypy o ty y Pydantic para configuraciones
    3. 0.4.3 Iteradores, generadores, decoradores y context managers
    4. 0.4.4 Ficheros (CSV, JSON, YAML, TOML) y fechas con zonas horarias
    5. 0.4.5 Logging profesional y testing con pytest
    6. 0.4.6 Concurrencia: threading, multiprocessing, asyncio y free-threading
  5. 0.5 · Computación numérica y datos

    1. 0.5.1 NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios
    2. 0.5.2 pandas 3.0: Series, DataFrame, índices temporales, Copy-on-Write, tipo str y pd.col()
    3. 0.5.3 Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles
    4. 0.5.4 Polars: API lazy frente a eager, expresiones, rendimiento y cuándo elegirlo frente a pandas
    5. 0.5.5 DuckDB y SQL esencial: SELECT, JOIN, GROUP BY y funciones de ventana sobre Parquet
  6. 0.6 · Visualización

    1. 0.6.1 Matplotlib orientado a objetos, Seaborn y Plotly: gráficos de precios, drawdown y velas
    2. 0.6.2 Dashboards rápidos con Streamlit: un panel de seguimiento en un solo script
  7. 0.7 · Matemáticas y estadística mínimas

    1. 0.7.1 Rentabilidades simples y logarítmicas, interés compuesto y estadística descriptiva
    2. 0.7.2 Probabilidad y distribuciones, correlación frente a causalidad y regresión simple
    3. 0.7.3 Álgebra lineal intuitiva: vectores y matrices como preparación para carteras

Seleccionar: loc e iloc

La lección 0.5.2 presentó las Series y los DataFrame. Para trabajar con ellos hay dos selectores, y confundirlos es uno de los errores más comunes:

SelectorSelecciona porEjemploCorte a:b
locEtiqueta: fechas, nombres de columnaprecios.loc["2023-01-02", "cierre"]Incluye b
ilocPosición entera, desde 0precios.iloc[-1, 3]Excluye b, como Python

Los dos aceptan [filas, columnas], y cada parte puede ser una etiqueta (o posición), una lista, un corte o una máscara de bool:

IN [01]python
import numpy as np
import pandas as pd

precios = pd.read_csv("sint_diario.csv", parse_dates=["fecha"],
                      index_col="fecha")
cierre = precios["cierre"]
r = cierre.pct_change()

print(precios.loc["2023-01-02", "cierre"])
print(precios.iloc[-1, 3], precios.columns[3])
precios.iloc[-3:, [3, 4]]
OUT [01]loc e iloc sobre las barras de SINT
105.44
142.22 cierre
            cierre  volumen
fecha
2025-12-29  139.56   801992
2025-12-30  139.98  1510567
2025-12-31  142.22  2459297

La última fila tiene posición −1 y la columna cierre es la cuarta (posición 3). iloc es útil para «las últimas n sesiones» o «la primera fila»; para todo lo demás, loc con fechas y nombres es más legible y no se rompe si cambia el orden de las columnas.

Filtrar sesiones

Una máscara de bool del mismo índice selecciona las filas en que vale True, como en NumPy (lección 0.5.1). Las condiciones se combinan con & (y), | (o) y ~ (no), cada una entre paréntesis: & tiene más prioridad que <, y and/or de Python no funcionan con máscaras.

IN [02]python
volumen_alto = precios["volumen"] > precios["volumen"].median()
caidas = precios[(r < -0.03) & volumen_alto]
print(len(caidas), "sesiones")
print(caidas.index.year.value_counts().sort_index().to_dict())

ops = pd.read_csv("operaciones_sint.csv",
                  parse_dates=["fecha_entrada", "fecha_salida"])
otras = ops[~ops["simbolo"].isin(["SINT"])]
print(otras["simbolo"].value_counts().to_dict())
OUT [02]Caídas fuertes con volumen alto
10 sesiones
{2021: 1, 2022: 8, 2025: 1}
{'RVAR': 4, 'ORO': 3, 'TECH': 3}

De las diez sesiones con caída de más del 3 % y volumen por encima de la mediana, la mayoría están en 2022, el año de la caída de SINT. isin comprueba la pertenencia a una lista y ~ la niega: diez operaciones del libro son de otros activos de la cartera ficticia.

Cambiar de frecuencia: resample

resample agrupa un índice de fechas en periodos (semanas, meses, años) y aplica una agregación a cada uno. Para rentabilidades mensuales se toma el último cierre de cada mes y se calcula su variación:

IN [03]python
cierre_mes = cierre.resample("ME").last()
r_mes = cierre_mes.pct_change()
print(len(r_mes), r_mes.index[0].date(), r_mes.index[-1].date())
r_mes.head(3).round(4)
OUT [03]Rentabilidades mensuales
60 2021-01-31 2025-12-31
fecha
2021-01-31       NaN
2021-02-28    0.0173
2021-03-31    0.0576
Freq: ME, Name: cierre, dtype: float64

"ME" significa month end: la etiqueta de cada mes es su último día natural, aunque la última sesión fuera antes. La rentabilidad de enero de 2021 es NaN porque no hay un mes anterior: los datos empiezan el 4 de enero. El alias antiguo "M" ya no se admite en pandas 3.0:

IN [04]python
cierre.resample("M").last()
OUT [04]El alias M ya no existe
ValueError: Invalid frequency: M. Failed to parse with error message: ValueError("'M' is no longer supported for offsets. Please use 'ME' instead.")

Componer las rentabilidades diarias de cada mes con (1 + r).resample("ME").prod() - 1 da el mismo resultado. Lo que no da lo mismo es la media de los cierres del mes: es otra serie, más suave, que no corresponde a ningún precio al que se pudiera operar.

La etiqueta del periodo también importa. El valor de marzo de 2022 lleva la fecha del 31 de marzo y usa el cierre de ese día: no se conoce hasta que termina el mes. Si después se cruza esta serie con datos diarios, el dato mensual solo puede usarse a partir de la sesión siguiente. En una señal que se calcula «a principios de mes» con el dato del mes que acaba de empezar se cuela la información de cuatro semanas futuras, y el backtest lo agradece de forma engañosa. resample tiene los parámetros label y closed para elegir qué extremo del periodo da nombre a cada grupo; con datos de precios, lo seguro es dejar los valores por defecto y desplazar con shift(1) lo que se vaya a usar como señal.

Agrupar: groupby

groupby sigue el patrón dividir, aplicar, combinar: separa las filas en grupos según una clave, aplica una función a cada grupo y junta los resultados en una tabla con una fila por grupo. La clave puede ser una columna, un atributo del índice o cualquier Series alineada:

RentabilidadesDividir por añoAplicar aggTabla por año
Fuente Mermaid
flowchart TB
  A[Rentabilidades] --> B[Dividir por año]
  B --> C[Aplicar agg]
  C --> D[Tabla por año]
IN [05]python
por_ano = r.groupby(r.index.year).agg(["mean", "std", "count"])
por_ano["anual"] = (1 + r).groupby(r.index.year).prod() - 1
por_ano["vol_anual"] = por_ano["std"] * np.sqrt(252)
por_ano[["count", "anual", "vol_anual"]].round(4)
OUT [05]Rentabilidad y volatilidad por año
       count   anual  vol_anual
fecha
2021     259  0.1520     0.1599
2022     260 -0.0850     0.2483
2023     260  0.1421     0.1495
2024     262  0.1432     0.1499
2025     261  0.0424     0.1792

2022 es el único año en negativo (−8,5 %) y el de mayor volatilidad (24,8 %, frente a un 15–18 % en los demás). La columna count es un control rápido: 259 rentabilidades en 2021 porque la primera sesión no tiene anterior.

El mismo patrón con el día de la semana como clave es el primer paso de un estudio de estacionalidad:

IN [06]python
dias = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
semana = r.groupby(r.index.day_name()).agg(["mean", "count"])
semana = semana.reindex(dias)
semana["mean"] = (semana["mean"] * 100).round(3)
semana
OUT [06]Rentabilidad media por día de la semana
            mean  count
fecha
Monday     0.045    260
Tuesday   -0.052    261
Wednesday  0.019    261
Thursday   0.051    260
Friday     0.108    260

El viernes sale con la mayor media (0,108 % diario) y el martes en negativo. Antes de convertirlo en una regla, una cuenta: el error típico de una media de 260 rentabilidades con una desviación típica del 1,14 % es 0,0114/260≈0,07 %0{,}0114 / \sqrt{260} \approx 0{,}07\ \%. Las diferencias entre días son de ese orden: compatibles con el azar. En SINT no puede haber efecto día de la semana, porque los datos se generan sin él; en datos reales, la misma tabla produce «patrones» igual de convincentes y casi siempre igual de falsos (pruebas múltiples, nivel 2).

Pivotar: una tabla año × mes

pivot reorganiza una tabla larga (una fila por mes) en una tabla ancha: una fila por año, una columna por mes. Es la tabla de rentabilidades mensuales que acompaña a cualquier informe de un fondo:

IN [07]python
larga = r_mes.to_frame("r").assign(
    ano=r_mes.index.year, mes=r_mes.index.month,
)
tabla = larga.pivot(index="ano", columns="mes", values="r")
(tabla * 100).round(1)
OUT [07]Rentabilidades mensuales por año
mes    1    2     3    4    5    6    7     8    9    10   11   12
ano
2021  NaN  1.7   5.8  1.5 -5.0  7.2 -1.7   3.0  2.2  0.5 -3.5  6.6
2022 -3.6  8.4  -6.8 -9.9  7.2 -0.0 -3.9  10.9 -9.5 -3.1 -5.1  9.6
2023  2.6 -1.9   2.7  0.6  3.1 -0.4 -3.1   0.5  6.2 -0.5  2.4  1.5
2024  8.2 -1.9  -2.6  0.9  1.4  1.0  2.0  -5.4  7.3 -2.9 -0.9  7.4
2025  3.6  1.9 -10.1  5.6 -0.7 -2.1 -1.3   2.0  2.7  1.0  4.6 -2.1

Leída por filas, la tabla cuenta la historia de 2022: cinco meses con movimientos de más del 8 % en un sentido u otro, de la caída del 9,9 % de abril a la subida del 10,9 % de agosto. Leída por columnas, sugiere tentaciones (¿diciembre sube casi siempre?) con cinco datos por columna: demasiado pocos para concluir nada. Si la tabla larga tuviera dos filas con el mismo año y mes, pivot lanzaría un error; pivot_table las agregaría con una función (por defecto, la media).

Combinar tablas: merge y join

El libro de operaciones guarda fechas y precios de entrada, pero no el volumen de esos días ni cómo cerró el mercado. merge combina dos tablas por columnas comunes, como el JOIN de SQL (lección 0.5.5). Para las operaciones de SINT, la clave es la fecha de entrada en la izquierda y el índice de precios en la derecha:

IN [08]python
ops_sint = ops[ops["simbolo"] == "SINT"]
con_precio = ops_sint.merge(
    precios[["cierre", "volumen"]],
    left_on="fecha_entrada", right_index=True,
    how="left", validate="many_to_one",
)
print(len(ops_sint), len(con_precio))
iguales = (con_precio["precio_entrada"] == con_precio["cierre"]).all()
print("Entrada al cierre del día:", iguales)
con_precio[["id", "fecha_entrada", "lado", "cierre", "volumen"]].head(3)
OUT [08]Operaciones con los precios del día de entrada
30 30
Entrada al cierre del día: True
   id fecha_entrada   lado  cierre  volumen
1   2    2021-09-02  largo  108.64  1454984
2   3    2021-10-27  corto  112.07  1644654
3   4    2021-11-10  corto  104.30  2575784

Tres controles en una celda. how="left" conserva todas las operaciones aunque falte su fecha en precios (saldría NaN); validate="many_to_one" lanza un error si una fecha aparece dos veces en precios, que duplicaría operaciones en silencio; y el número de filas antes y después debe coincidir: 30 y 30. La comparación final confirma lo que dice el README de los datos: las entradas se hicieron al precio de cierre.

ArgumentoValoresPara qué
how"inner", "left", "right", "outer"Qué filas sin pareja se conservan
on / left_on / right_onColumnasClaves de cada lado
left_index / right_indexTrueUsar el índice como clave
validate"one_to_one", "many_to_one"…Error si la relación no es la esperada
indicatorTrueColumna _merge con el origen de cada fila

Cuando las dos tablas comparten el índice de fechas, join es más corto: une por índice y, por defecto, conserva las filas de la izquierda. SINT y los cuatro activos de la cartera tienen el mismo calendario:

IN [09]python
cartera = pd.read_csv("cartera_sint.csv", parse_dates=["fecha"],
                      index_col="fecha")
todos = precios[["cierre"]].rename(columns={"cierre": "SINT"})
todos = todos.join(cartera)
print(todos.shape, todos.isna().sum().sum())
todos.pct_change().corr().round(2).loc["SINT"]
OUT [09]join por índice de fechas
(1303, 5) 0
SINT    1.00
RVAR   -0.01
RFIJ    0.00
ORO     0.00
TECH   -0.02
Name: SINT, dtype: float64

1.303 filas y ningún ausente: los calendarios coinciden. Con datos de dos mercados distintos no sería así, y la decisión de qué hacer con las fechas sin pareja (lección 0.5.2) se toma aquí: how="inner" se queda con las sesiones comunes, how="outer" con todas (y NaN donde falte un mercado), y ffill() arrastra el último precio conocido de un mercado cerrado, que es lo que vería un gestor al valorar la cartera ese día. SINT apenas está correlacionado con los activos de la cartera: son series ficticias generadas por separado.

Ventanas: rolling y expanding

Una ventana móvil calcula un estadístico sobre las últimas n filas y avanza una fila cada vez. Es la herramienta de las medias móviles, las volatilidades móviles y las bandas de cualquier indicador. La volatilidad anualizada de los últimos 21 días (un mes de mercado) es:

σt=252⋅std⁡(rt−20,…,rt)\sigma_t = \sqrt{252} \cdot \operatorname{std}(r_{t-20}, \dots, r_t)
IN [10]python
vol_21 = r.rolling(21).std() * np.sqrt(252)
print(vol_21.isna().sum(), "valores sin ventana completa")
print(f"Mediana: {vol_21.median():.1%}")
print(f"Máxima:  {vol_21.max():.1%} el {vol_21.idxmax().date()}")
print(f"Mínima:  {vol_21.min():.1%} el {vol_21.idxmin().date()}")
OUT [10]Volatilidad móvil de 21 sesiones
21 valores sin ventana completa
Mediana: 15.8%
Máxima:  33.9% el 2022-09-12
Mínima:  7.9% el 2021-11-03

La ventana está alineada a la derecha: el valor del 12 de septiembre de 2022 usa las 21 rentabilidades que terminan ese día, incluida. Los 21 primeros valores son NaN: el primero porque no hay rentabilidad y los otros veinte porque la ventana no está completa (con min_periods se puede pedir un cálculo con menos datos). El tamaño de la ventana es un compromiso: con 21 sesiones la estimación reacciona en pocos días a un cambio de régimen, pero es ruidosa; con 63 o 126 es más estable y llega tarde. No hay una ventana correcta, y elegir la que mejor resultado da en un backtest es otra forma de sobreajuste. La figura de la cabecera dibuja esta serie: la volatilidad pasa largos periodos en torno a su mediana del 15,8 % y se dispara en dos episodios, en 2022 y en abril de 2025, hasta duplicarla.

Una ventana expansiva (expanding) empieza en la primera fila y crece hasta la actual. Su uso más común en trading es el máximo histórico de los precios, que define el drawdown: la caída desde el máximo alcanzado hasta ese momento.

DDt=Ptmax⁡s≤tPs−1\text{DD}_t = \frac{P_t}{\max_{s \le t} P_s} - 1
IN [11]python
maximo = cierre.expanding().max()
print((maximo == cierre.cummax()).all())
drawdown = cierre / maximo - 1
fondo = drawdown.idxmin()
pico = cierre.loc[:fondo].idxmax()
print(f"Peor drawdown: {drawdown.min():.1%}")
print(f"Del máximo del {pico.date()} al fondo del {fondo.date()}")
print(f"Sesiones en drawdown: {(drawdown < 0).mean():.0%}")
OUT [11]Máximo acumulado y drawdown
True
Peor drawdown: -25.0%
Del máximo del 2022-03-07 al fondo del 2022-10-20
Sesiones en drawdown: 95%

expanding().max() y cummax() son lo mismo; cummax es más corto. El peor drawdown de SINT es del 25 %: del máximo de marzo de 2022 al fondo de octubre. Y el 95 % de las sesiones está por debajo del máximo previo: lo normal en un activo, o en una estrategia, es estar en drawdown. La lección 0.6.1 dibuja esta curva bajo los precios.

Ejercicios

  1. Construye las barras semanales de SINT (apertura, máximo, mínimo, cierre y volumen) con resample("W-FRI") y agg, con la agregación correcta para cada columna. Muestra las tres primeras.
Solución
IN [12]python
semanal = precios.resample("W-FRI").agg({
    "apertura": "first", "maximo": "max", "minimo": "min",
    "cierre": "last", "volumen": "sum",
})
print(len(semanal))
semanal.head(3)
OUT [12]Barras semanales
261
            apertura  maximo  minimo  cierre   volumen
fecha
2021-01-08    100.02  100.64   97.22   99.05  10647985
2021-01-15     98.98   99.70   96.96   97.75   7135061
2021-01-22     97.68   99.32   96.02   98.36   7573689

La apertura de la semana es la del primer día; el cierre, el del último; el máximo y el mínimo, los extremos de la semana; el volumen, la suma. "W-FRI" cierra las semanas en viernes y las etiqueta con esa fecha.

  1. Calcula el resultado neto de cada operación del libro (signo × diferencia de precios × cantidad − comisión) y, con groupby, una tabla por símbolo con el número de operaciones, el resultado total y el porcentaje de ganadoras.
Solución
IN [13]python
signo = np.where(ops["lado"] == "largo", 1, -1)
ops["pnl"] = (signo * (ops["precio_salida"] - ops["precio_entrada"])
              * ops["cantidad"] - ops["comision"])
resumen = ops.groupby("simbolo").agg(
    operaciones=("pnl", "size"),
    total=("pnl", "sum"),
    ganadoras=("pnl", lambda s: (s > 0).mean()),
)
resumen.round(2)
OUT [13]Resultado por símbolo
         operaciones    total  ganadoras
simbolo
ORO                3 -3237.96       0.00
RVAR               4    17.09       0.50
SINT              30  4405.08       0.63
TECH               3   -35.02       0.33

La sintaxis nombre=(columna, función) de agg pone nombre a cada columna del resultado. El porcentaje de ganadoras es la media de una máscara de bool. Con tres o cuatro operaciones por símbolo fuera de SINT, ninguna de estas cifras dice nada sobre la calidad de las reglas que las generaron.

  1. ¿Cuántas sesiones de cada año tuvieron una volatilidad de 21 sesiones por encima del 25 %? Usa vol_21 y groupby.
Solución
IN [14]python
alta = vol_21 > 0.25
alta.groupby(alta.index.year).sum()
OUT [14]Sesiones de volatilidad alta por año
fecha
2021      0
2022    133
2023      0
2024      0
2025     31
Name: cierre, dtype: int64

Sumar una máscara cuenta los True. La mayoría de las sesiones de volatilidad alta están en 2022; el repunte de 2025 es más corto. Los NaN del principio cuentan como False en la comparación (lección 0.5.1), así que no suman.

Respuesta plegada en cada pregunta

Autoevaluación

  1. precios tiene un DatetimeIndex. ¿Qué devuelve precios.iloc[-1, 3]?

    • ALa fila con etiqueta −1 y la columna con etiqueta 3
    • BEl valor de la última fila y la cuarta columna, por posición
    • CUn error, porque el índice es de fechas
    • DLas tres últimas filas
    Ver respuesta

    Respuesta B. El valor de la última fila y la cuarta columna, por posición

    iloc usa posiciones, empezando en 0, como las listas de Python: −1 es la última fila y 3, la cuarta columna. loc usaría etiquetas (fechas y nombres de columna).

  2. ¿Cuál de estas expresiones filtra las sesiones con caída mayor del 3 % y volumen por encima de la mediana?

    • Ap[r < -0.03 and p["volumen"] > med]
    • Bp[(r < -0.03) & (p["volumen"] > med)]
    • Cp[r < -0.03 & p["volumen"] > med]
    • Dp[(r < -0.03) or (p["volumen"] > med)]
    Ver respuesta

    Respuesta B. p[(r < -0.03) & (p["volumen"] > med)]

    Con máscaras de pandas se usan &, | y ~, no and/or, y cada condición va entre paréntesis porque & tiene más prioridad que < y >.

  3. r.rolling(21).std() en la fecha t, ¿qué rentabilidades usa?

    • ALas 21 sesiones centradas en t
    • BLas 21 sesiones siguientes a t
    • CLas 21 sesiones que terminan en t, incluida t
    • DTodas las sesiones hasta t
    Ver respuesta

    Respuesta C. Las 21 sesiones que terminan en t, incluida t

    Por defecto la ventana está alineada a la derecha: termina en la fila actual. Con center=True usaría sesiones futuras, y expanding() usaría todas las anteriores.

  4. Al combinar operaciones con precios por fecha, ¿para qué sirve validate="many_to_one" en merge?

    • APara eliminar las operaciones duplicadas
    • BPara lanzar un error si una fecha aparece más de una vez en la tabla de precios, en lugar de duplicar operaciones en silencio
    • CPara que el merge sea más rápido
    • DPara conservar solo las fechas comunes
    Ver respuesta

    Respuesta B. Para lanzar un error si una fecha aparece más de una vez en la tabla de precios, en lugar de duplicar operaciones en silencio

    Si la tabla de la derecha tuviera dos filas con la misma fecha, cada operación de ese día se duplicaría y el resultado total cambiaría sin avisar. validate convierte ese error de datos en una excepción.

Ideas clave

  • loc selecciona por etiqueta (fechas y nombres) con los extremos incluidos; iloc, por posición. Las máscaras se combinan con &, | y ~, cada condición entre paréntesis.
  • resample("ME") cambia de frecuencia por fecha; groupby agrupa por cualquier clave (año, día de la semana, símbolo) y aplica una agregación a cada grupo.
  • merge combina tablas por columnas y join, por índice. validate y indicator detectan duplicados y filas sin pareja antes de que falseen un resultado.
  • rolling(n) usa las n filas que terminan en la actual y expanding() todas las anteriores: ninguna mira al futuro. center=True sí, y no se usa para señales.
  • Volatilidad móvil: r.rolling(21).std() * np.sqrt(252). Drawdown: cierre / cierre.cummax() - 1.

Recursos

Datos