Reproduzir a história
Um backtest pergunta o que uma regra de trading teria ganhado com dados passados. A versão vetorizada trabalha com arrays inteiros: sem laço sobre os dias, só um sinal por dia, uma posição por dia e uma multiplicação com os retornos. É curta, rápida e fácil de verificar, o que a torna a primeira ferramenta para filtrar ideias. Seus limites (sem ordens, sem caixa, sem execuções parciais) são exatamente o que o próximo tópico acrescenta.
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)O atraso de um dia
Um sinal calculado com o fechamento de hoje só pode ser negociado depois desse fechamento, então ganha o retorno de amanhã, não o de hoje. Em arrays isso é um deslocamento: strategy[t] = position[t - 1] * returns[t], sem posição antes do primeiro dia. Esquecê-lo deixa a estratégia negociar com um retorno que já viu, e o backtest parece maravilhoso. O deslocamento é uma linha, a mais importante de toda a função.
# 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 e estatísticas
Os retornos diários se compõem: o valor após cada dia é o inicial vezes o produto acumulado de 1 + retorno. Dessa curva de capital, três números resumem um teste. O retorno total é o valor final menos 1. O índice de Sharpe é o retorno diário médio dividido pelo desvio-padrão, vezes sqrt(periods) para anualizar. O drawdown máximo é a pior queda a partir de um pico anterior, equity / running max - 1, a perda que um investidor teria vivido.
# 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))- Posição = sinal do sinal; o retorno da estratégia é
position[t - 1] * returns[t]: o retorno de hoje pertence à decisão de ontem. - O capital é
start * cumprod(1 + r); retorno total, Sharpe anualizado e drawdown máximo o resumem. - Backtests vetorizados são filtros rápidos: ignoram custos e execuções, e muitas tentativas num histórico produzem sorte.
A seguir: um backtester orientado a eventos, reproduzir a história barra a barra com caixa, ordens e execuções.
Lab: o que é avaliado
- Transformar um sinal em posição com
signal_to_position25 pts - Ganhar com a posição de ontem com
strategy_returns25 pts - Compor os dias com
equity_curve25 pts - Resumir o teste com
backtest_stats25 pts