Saber demais
O viés de antecipação (look-ahead) ocorre quando um backtest usa informação que não estava disponível no momento da decisão. É a razão mais comum de uma estratégia parecer brilhante no papel e perder dinheiro ao vivo. Raramente é uma linha óbvia; esconde-se no alinhamento (um sinal do fechamento de hoje negociado nesse fechamento), em estatísticas calculadas sobre toda a amostra, em dados revisados depois e em índices escolhidos com retrospectiva. O remédio é um hábito: para cada número que uma decisão usa, pergunte quando ele se tornou conhecido.
import numpy as np
rng = np.random.default_rng(5)
returns = rng.normal(0, 0.01, size=250) # pure noise: no strategy can really make money here
signal = returns.copy() # "signal" = the return of the same day
position = np.sign(signal)
biased = np.prod(1 + position * returns) - 1 # trades on the return it is about to earn
honest_position = np.concatenate([[0.0], position[:-1]])
honest = np.prod(1 + honest_position * returns) - 1
print(round(float(biased), 2), round(float(honest), 2)) # 6.0 0.01: a 600% "return" on noise, against 1%Alinhamento: o atraso de um dia
O remédio para o viés de alinhamento é um atraso: um valor calculado no dia t só pode ser usado a partir do dia t + 1. Em arrays é um deslocamento com zeros no início, out[t] = signal[t - periods]. O teste é direto. Uma série de ruído não tem vantagem, então qualquer regra que faça fortuna com ruído está trapaceando. Compare a versão enviesada com a atrasada nos mesmos dados, e a diferença é o tamanho do viés.
# lag_signal: delay a series, with zeros where there is nothing to look back at
import numpy as np
def lag_signal(signal, periods=1):
out = np.zeros(len(signal))
if periods < len(signal):
out[periods:] = signal[:len(signal) - periods]
return out
s = np.array([1.0, -1.0, 1.0, 1.0, -1.0])
print(lag_signal(s)) # [ 0. 1. -1. 1. 1.]
print(lag_signal(s, 2)) # [ 0. 0. 1. -1. 1.]# trailing_mean: the mean of the last `window` values, using only today and the past
import numpy as np
def trailing_mean(values, window):
total = np.cumsum(values)
out = np.full(len(values), np.nan)
out[window - 1:] = (total[window - 1:] - np.concatenate([[0.0], total[:len(values) - window]])) / window
return out
x = np.array([1.0, 2.0, 6.0, 7.0])
print(trailing_mean(x, 2)) # [ nan 1.5 4. 6.5]
print(trailing_mean(x[:3], 2)) # [ nan 1.5 4. ]: adding day 4 changed nothing before itAs estatísticas devem crescer com o tempo
O segundo vazamento é sutil. Padronizar uma série com a média e o desvio de toda a amostra usa números que só existem no final: os primeiros valores são escalados com informação do futuro. Use só o passado: uma janela móvel (os últimos n valores até hoje) ou crescente (tudo até hoje). O teste para qualquer variável assim: calculá-la só nos primeiros k dias deve dar os primeiros k valores do cálculo completo.
# Whole-sample z-scores leak the future; expanding ones do not
import numpy as np
x = np.array([1.0, 3.0, 2.0, 8.0, 4.0])
whole = (x - x.mean()) / x.std()
n = np.arange(1, len(x) + 1)
mean = np.cumsum(x) / n
var = np.cumsum(x ** 2) / n - mean ** 2
with np.errstate(divide="ignore", invalid="ignore"):
expanding = (x - mean) / np.sqrt(var)
expanding[0] = np.nan
print(np.round(whole, 3))
print(np.round(expanding, 3))
# Prefix test: computing on x[:3] gives the first three expanding values, but not the first three whole-sample ones
print(np.round((x[:3] - x[:3].mean()) / x[:3].std(), 3))- O viés de antecipação é qualquer uso de informação antes de ela ser conhecida; o alinhamento é a forma clássica: um sinal do dia
tnegocia a partir do diat + 1. - As variáveis devem ser construídas só com o passado: janelas móveis ou crescentes, nunca a média ou o desvio da amostra toda.
- Teste do prefixo: uma variável ou backtest nos primeiros
kdias deve igualar os primeiroskresultados da execução completa; uma série de ruído não deve dar dinheiro.
A seguir: os custos de transação, a primeira coisa que um backtest limpo precisa pagar.
Lab: o que é avaliado
- Atrasar um sinal com
lag_signal25 pts - Medir a trapaça com
biased_vs_honest25 pts - Fazer a média só do passado com
trailing_mean25 pts - Padronizar sem espiar com
expanding_zscore25 pts