Un backtest vectorizado: señales, posiciones, desplazamiento para evitar el sesgo de anticipación, costes de transacción y la curva de capital
Construye en pandas un backtest vectorizado completo de una regla de media móvil, con el desplazamiento de la posición, los costes por operación, la curva de capital y una comparación con un índice de referencia.
Qué aprenderás
- La estructura de cinco columnas de un backtest vectorizado: señal, posición, rentabilidad del activo, coste y rentabilidad de la estrategia.
- Por qué
shift(1)en la columna de posición es la única línea que separa una prueba honesta de una fantasía con sesgo de anticipación. - Cómo cargar costes de transacción proporcionales en los cambios de posición y construir una curva de capital en euros.
- Qué puede y qué no puede modelar una prueba vectorizada, y cuándo se necesita el motor basado en eventos de la lección 8.
Explicación
Un backtest vectorizado calcula todo el historial de una vez con operaciones sobre columnas, en lugar de recorrer las barras una a una en un bucle. El backtest en hoja de cálculo del curso Trading sistemático y backtesting tenía esta estructura; pandas lo ejecuta sobre décadas en milisegundos. Es adecuado para reglas largo-fuera o siempre invertidas que actúan sobre el cierre; los stops dentro de la barra y los precios de ejecución concretos son tarea de la lección 8.
Paso 1, señal. signal = (close > sma(close, 50)).astype(int) da 1 cuando el cierre está por encima de su media de 50 días y 0 en caso contrario. Describe lo que sabes al cierre de la barra t.
Paso 2, posición. No puedes operar con un cierre que aún no has visto; lo antes que puedes mantener la señal de la barra t es en la barra t + 1, así que position = signal.shift(1).fillna(0). La fila t de la columna de posición significa ahora «la exposición mantenida durante la barra t», decidida al final de la barra t − 1. Omitir el desplazamiento permite que la estrategia gane la rentabilidad de la barra t usando el cierre de la barra t, lo que constituye el sesgo de anticipación (look-ahead bias): usar información no disponible en el momento de la decisión.
Paso 3, rentabilidad del activo. ret = close.pct_change().fillna(0) (lección 5). Como la posición durante la barra t se fijó al cierre de la barra t − 1, position * ret es la rentabilidad bruta de la estrategia en la barra t. El supuesto implícito es una ejecución al cierre de la barra t − 1; la lección 8 trata las ejecuciones en la apertura siguiente.
Paso 4, costes. La comisión, el spread y el deslizamiento se modelan como una tasa proporcional por unidad de posición modificada: turnover = position.diff().abs().fillna(0) vale 1 al pasar de fuera a largo o al revés, y 2 en una inversión. cost = turnover * c, donde c es el coste por sentido como fracción (0.001 para un 0.1%). El coste entra en la columna de rentabilidad en la barra en la que cambia la posición.
Paso 5, rentabilidad de la estrategia y capital. strat = position * ret - cost, y equity = capital * (1 + strat).cumprod(). La referencia de comprar y mantener es capital * (1 + ret).cumprod(). Compara siempre ambas con las mismas métricas (lección 9); limitarse a diluir el comprar y mantener con efectivo reduce por igual el drawdown y la rentabilidad, y eso no es habilidad.
Indica los supuestos: la exposición es una fracción del capital reinvertida a diario, ejecuciones al cierre, costes proporcionales, sin intereses sobre el efectivo, dividendos solo a través de datos ajustados, impuestos ignorados. La prueba responde entonces a una pregunta concreta: ¿aportó algo la regla de temporización frente a mantener el activo, después de costes?
Comprobaciones antes de fiarte del resultado. (1) Pon el coste a cero y la señal a un 1 constante; el capital debe ser exactamente igual al de comprar y mantener. (2) Cuenta las operaciones de ida y vuelta con turnover.sum() / 2 y comprueba que el número es plausible. (3) Asegúrate de que las filas NaN del periodo de calentamiento del indicador dan una posición cero, no una operación espuria.
Ejemplo práctico
Ejemplo hipotético: ocho cierres en euros, 100, 101, 103, 102, 104, 103, 101, 100; largo cuando el cierre está estrictamente por encima de su SMA de 3 días y fuera en caso contrario; coste por sentido del 0.1%; capital de €10,000; sin intereses, dividendos ni impuestos.
| Barra | Cierre | SMA3 | Señal | Posición | Rentabilidad | Rotación | Estrategia | Capital € |
|---|---|---|---|---|---|---|---|---|
| 0 | 100 | – | 0 | 0 | 0 | 0 | 0 | 10,000.00 |
| 1 | 101 | – | 0 | 0 | +1.000% | 0 | 0 | 10,000.00 |
| 2 | 103 | 101.33 | 1 | 0 | +1.980% | 0 | 0 | 10,000.00 |
| 3 | 102 | 102.00 | 0 | 1 | −0.971% | 1 | −1.071% | 9,892.91 |
| 4 | 104 | 103.00 | 1 | 0 | +1.961% | 1 | −0.100% | 9,883.02 |
| 5 | 103 | 103.00 | 0 | 1 | −0.962% | 1 | −1.062% | 9,778.11 |
| 6 | 101 | 102.67 | 0 | 0 | −1.942% | 1 | −0.100% | 9,768.33 |
| 7 | 100 | 101.33 | 0 | 0 | −0.990% | 0 | 0 | 9,768.33 |
Barra 3: la señal en la barra 2 era 1 (103 > 101.33), así que la posición durante la barra 3 es 1. El activo bajó de 103 a 102 (−0.971%), la rotación de 1 cuesta un 0.1%, la rentabilidad de la estrategia es −1.071% y el capital, 10,000 × (1 − 0.01071) = €9,892.91. En la barra 3 el cierre es igual a la SMA, así que la regla estricta da 0 y la posición se cierra durante la barra 4 (coste del 0.1%, sin exposición). Capital final de €9,768.33 (−2.32%) frente a exactamente €10,000 con comprar y mantener. Los costes se llevaron €39.36 en cuatro operaciones por sentido.
Ahora, la versión con sesgo de anticipación, usando signal en lugar de la position desplazada: la estrategia «mantiene» durante las barras 2 y 4, los dos días alcistas que solo podía identificar después de su cierre, y termina en €10,356.65 (+3.57%). Un solo shift(1) olvidado convirtió un −2.32% en un +3.57%.
Error frecuente
position = signal.shift(1); las rentabilidades son un simple pct_change().Actividad práctica
En papel, con los mismos ocho cierres y la misma regla: (a) vuelve a calcular el capital final con un coste por sentido del 0.2%; (b) indica el número de operaciones de ida y vuelta; (c) un colega propone rellenar los primeros valores NaN de la SMA con el cierre para que «se pueda operar desde el primer día»; explica brevemente por qué esto es un problema.
Mostrar solución
(a) Solo cambian las cuatro barras con rotación. Barra 3: −1.171% → €9,882.91; barra 4: −0.2% → €9,863.15; barra 5: −1.162% → €9,748.58; barra 6: −0.2% → €9,729.09. Pérdida final del 2.71% frente al 2.32% con un 0.1%; duplicar la tasa de coste añadió €39.24 de lastre.
(b) La rotación suma 4 cambios por sentido, es decir, 2 operaciones de ida y vuelta (entrada durante la barra 3 y salida en la barra 4; entrada en la barra 5 y salida en la barra 6).
(c) Rellenar la SMA con el cierre crea operaciones a partir de un indicador que todavía no existe, así que su lógica difiere de la regla que se está probando; mantén una posición cero durante el calentamiento y anota cuántas barras se excluyeron.
Limitaciones: ocho barras demuestran la mecánica, no la regla; los costes proporcionales y las ejecuciones al cierre subestiman el deslizamiento en instrumentos poco líquidos.
Ideas clave
- Un backtest vectorizado son cinco columnas: señal, posición (desplazada), rentabilidad del activo, coste sobre la rotación y rentabilidad de la estrategia; el capital es el producto acumulado.
position = signal.shift(1)es innegociable; compruébalo eliminándolo y observando cómo se infla el resultado.- Carga los costes sobre
position.diff().abs()y compara siempre con comprar y mantener bajo supuestos idénticos. - Las pruebas vectorizadas sirven para reglas de cierre a cierre; los stops, el dimensionamiento y las ejecuciones realistas necesitan el motor basado en eventos de la lección 8.
Siguiente paso
La lección 8 construye un backtest basado en eventos: un bucle barra a barra con stops por ATR, dimensionamiento de posiciones y una lista de operaciones medida en múltiplos R.
Autoevaluación
Responde a las preguntas y luego comprueba tus respuestas. Puedes volver a intentarlo tantas veces como quieras.
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.