Una barra cada vez
Un backtester dirigido por eventos se comporta como un sistema real: el tiempo avanza barra a barra, y en cada barra el programa solo ve el pasado, decide, envía una orden y actualiza su cuenta. Es más lento que uno vectorizado, pero mantiene estado (efectivo, acciones), puede modelar órdenes y ejecuciones, y el mismo código de estrategia podrá correr luego con datos en vivo. El bucle es lo esencial: un for sobre las barras, con la cuenta pasando de una iteración a la siguiente.
def crossover_signal(history, fast, slow):
if len(history) < slow:
return 0 # not enough history yet
fast_mean = sum(history[-fast:]) / fast
slow_mean = sum(history[-slow:]) / slow
return 1 if fast_mean > slow_mean else 0
prices = [100, 101, 103, 102, 105, 107, 104, 103, 99, 98]
print(crossover_signal(prices[:3], 2, 4)) # 0: only 3 prices
print(crossover_signal(prices[:6], 2, 4)) # 1: the 2-bar mean is above the 4-bar mean
print(crossover_signal(prices[:9], 2, 4)) # 0: the trend has turnedLas órdenes cambian la cuenta
Una orden convierte efectivo en acciones o al revés. Con acciones enteras, comprar int(cash // price) deja algo de efectivo, y el valor de la cuenta es siempre cash + shares * price. Mantener la lógica de órdenes en una pequeña función, execute_order, acorta el bucle y permite añadir comisiones y deslizamiento después en un solo lugar. Vender todo es simplemente cash + shares * price y cero acciones.
def execute_order(cash, shares, price, target):
if target == 1:
bought = int(cash // price) # whole shares only
return cash - bought * price, shares + bought
return cash + shares * price, 0
cash, shares = execute_order(1000.0, 0, 103.0, 1)
print(cash, shares, cash + shares * 103.0) # 73.0 9 1000.0
cash, shares = execute_order(cash, shares, 99.0, 0)
print(cash, shares) # 964.0 0El bucle y lo que puede saber
En la barra t la estrategia puede usar prices[:t], que termina en el cierre anterior, y opera a prices[t]. Cortar prices[:t] mantiene fuera el futuro: el siguiente precio simplemente no está en la lista. De ahí sale una buena prueba de un motor por eventos: ejecutarlo solo con las primeras k barras debe dar los primeros k resultados de la ejecución completa. Si difieren, la estrategia espió.
def crossover_signal(history, fast, slow):
if len(history) < slow:
return 0
return 1 if sum(history[-fast:]) / fast > sum(history[-slow:]) / slow else 0
def execute_order(cash, shares, price, target):
if target == 1:
bought = int(cash // price)
return cash - bought * price, shares + bought
return cash + shares * price, 0
def run_backtest(prices, fast, slow, cash):
shares = 0
values = []
for t, price in enumerate(prices):
if t > 0:
target = crossover_signal(prices[:t], fast, slow) # only what was known at the last close
cash, shares = execute_order(cash, shares, price, target)
values.append(cash + shares * price)
return values
prices = [100, 101, 103, 102, 105, 107, 104, 103, 99, 98]
full = run_backtest(prices, 2, 4, 1000.0)
print([round(v, 2) for v in full])
print(run_backtest(prices[:6], 2, 4, 1000.0) == full[:6]) # True: the future never leaks# The benchmark: buy once at the start and hold
import random
def buy_and_hold(prices, cash):
shares = int(cash // prices[0])
left = cash - shares * prices[0]
return [left + shares * price for price in prices]
random.seed(3)
prices = [100.0]
for _ in range(99):
prices.append(round(prices[-1] * (1 + random.gauss(0.0004, 0.012)), 2))
held = buy_and_hold(prices, 10000.0)
print(round(held[0], 2), round(held[-1], 2))- Un backtester por eventos recorre las barras con estado (efectivo, acciones); en la barra
tsolo puede usarprices[:t]. - Las órdenes mueven entre efectivo y acciones enteras: el valor de la cuenta es
cash + shares * price; mantenga la lógica de órdenes en una función. - Pruebe el look-ahead: una ejecución con las primeras
kbarras debe igualar los primeroskresultados de la completa; compare con comprar y mantener.
A continuación: el sesgo de anticipación, el error silencioso que hace un backtest demasiado bueno para ser cierto.
Lab: qué se califica
- Comparar dos medias con
crossover_signal25 pts - Ejecutar la orden con
execute_order25 pts - Recorrer las barras con
run_backtest25 pts - Fijar la referencia con
buy_and_hold25 pts