Uma barra de cada vez
Um backtester orientado a eventos se comporta como um sistema real: o tempo avança barra a barra, e em cada barra o programa vê só o passado, decide, envia uma ordem e atualiza a conta. É mais lento que um vetorizado, mas guarda estado (caixa, ações), pode modelar ordens e execuções, e o mesmo código de estratégia poderá depois rodar com dados ao vivo. O laço é o essencial: um for sobre as barras, com a conta passando de uma iteração à seguinte.
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 turnedAs ordens mudam a conta
Uma ordem transforma caixa em ações ou o contrário. Com ações inteiras, comprar int(cash // price) deixa um pouco de caixa, e o valor da conta é sempre cash + shares * price. Manter a lógica de ordens numa pequena função, execute_order, encurta o laço e permite acrescentar taxas e deslizamento depois em um só lugar. Vender tudo é simplesmente cash + shares * price e zero ações.
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 0O laço e o que ele pode saber
Na barra t a estratégia pode usar prices[:t], que termina no fechamento anterior, e negocia a prices[t]. Fatiar prices[:t] mantém o futuro de fora: o preço seguinte simplesmente não está na lista. Daí sai um bom teste de um motor por eventos: executá-lo só nas primeiras k barras deve dar os primeiros k resultados da execução completa. Se diferirem, a estratégia espiou.
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))- Um backtester por eventos percorre as barras com estado (caixa, ações); na barra
tsó pode usarprices[:t]. - As ordens movem entre caixa e ações inteiras: o valor da conta é
cash + shares * price; mantenha a lógica de ordens numa função. - Teste o look-ahead: uma execução nas primeiras
kbarras deve igualar os primeiroskresultados da completa; compare com comprar e manter.
A seguir: o viés de antecipação, o erro silencioso que torna um backtest bom demais para ser verdade.
Lab: o que é avaliado
- Comparar duas médias com
crossover_signal25 pts - Executar a ordem com
execute_order25 pts - Percorrer as barras com
run_backtest25 pts - Definir a referência com
buy_and_hold25 pts