Reproducir la historia
Un backtest pregunta qué habría ganado una regla de trading con datos pasados. La versión vectorizada trabaja con arrays enteros: sin bucle sobre los días, solo una señal por día, una posición por día y una multiplicación con los rendimientos. Es corta, rápida y fácil de comprobar, lo que la convierte en la primera herramienta para filtrar ideas. Sus límites (sin órdenes, sin efectivo, sin ejecuciones parciales) son justo lo que añade el siguiente tema.
import numpy as np
returns = np.array([0.01, 0.02, -0.01, 0.03]) # the asset's daily returns
signal = np.array([2.5, 0.4, -1.0, 0.0]) # any score: positive = buy, negative = sell
position = np.sign(signal) # +1, +1, -1, 0
print(position)El retraso de un día
Una señal calculada con el cierre de hoy solo puede operarse después de ese cierre, así que gana el rendimiento de mañana, no el de hoy. En arrays eso es un desplazamiento: strategy[t] = position[t - 1] * returns[t], sin posición antes del primer día. Olvidarlo deja que la estrategia opere con un rendimiento que ya ha visto, y el backtest parece maravilloso. El desplazamiento es una línea, la más importante de toda la función.
# Yesterday's position earns today's return
import numpy as np
returns = np.array([0.01, 0.02, -0.01, 0.03])
position = np.array([1.0, 1.0, -1.0, 0.0])
strategy = np.zeros(len(returns))
strategy[1:] = position[:-1] * returns[1:]
print(strategy) # [ 0. 0.02 -0.01 -0.03]
print(position * returns) # the same-day version: [ 0.01 0.02 0.01 0. ] looks better, and is cheatingCapital y estadísticas
Los rendimientos diarios se componen: el valor tras cada día es el inicial por el producto acumulado de 1 + rendimiento. De esta curva de capital, tres números resumen una prueba. El rendimiento total es el valor final menos 1. El ratio de Sharpe es el rendimiento diario medio dividido por su desviación típica, por sqrt(periods) para anualizarlo. El drawdown máximo es la peor caída desde un máximo previo, equity / running max - 1, es decir, la pérdida que habría vivido un inversor.
# Equity curve and the three statistics for a short run
import numpy as np
strategy = np.array([0.0, 0.02, -0.01, -0.03, 0.04])
equity = np.cumprod(1 + strategy)
print(np.round(equity, 5))
print(round(float(equity[-1] - 1), 5)) # total return
print(round(float(strategy.mean() / strategy.std(ddof=1) * np.sqrt(252)), 3)) # annualised Sharpe
print(round(float((equity / np.maximum.accumulate(equity) - 1).min()), 5)) # maximum drawdown# A moving-average rule on a simulated price path, with its statistics
import numpy as np
rng = np.random.default_rng(7)
returns = rng.normal(0.0004, 0.01, size=500)
prices = 100 * np.cumprod(1 + returns)
fast = np.convolve(prices, np.ones(10) / 10, mode="valid")[-(len(prices) - 49):] # 10-day mean, aligned
slow = np.convolve(prices, np.ones(50) / 50, mode="valid") # 50-day mean
position = np.zeros(len(prices))
position[49:] = np.sign(fast - slow)
strategy = np.zeros(len(returns))
strategy[1:] = position[:-1] * returns[1:]
equity = np.cumprod(1 + strategy)
drawdown = equity / np.maximum.accumulate(equity) - 1
print(round(float(equity[-1] - 1), 4), round(float(strategy.mean() / strategy.std(ddof=1) * np.sqrt(252)), 4), round(float(drawdown.min()), 4))- Posición = signo de la señal; el rendimiento de la estrategia es
position[t - 1] * returns[t]: el rendimiento de hoy corresponde a la decisión de ayer. - El capital es
start * cumprod(1 + r); rendimiento total, Sharpe anualizado y drawdown máximo lo resumen. - Los backtests vectorizados son filtros rápidos: ignoran costes y ejecuciones, y muchas pruebas sobre un historial producen suerte.
A continuación: un backtester dirigido por eventos, reproducir la historia barra a barra con efectivo, órdenes y ejecuciones.
Lab: qué se califica
- Convertir una señal en posición con
signal_to_position25 pts - Ganar con la posición de ayer con
strategy_returns25 pts - Componer los días con
equity_curve25 pts - Resumir la prueba con
backtest_stats25 pts