De exercícios a uma biblioteca
Você escreveu uma centena de funções pequenas. Uma biblioteca é essas funções organizadas para que outra pessoa (inclusive você daqui a seis meses) possa usá-las com confiança: uma interface pública clara, cada peça testada sozinha, as peças compostas num pipeline e tudo instalável, documentado e verificado automaticamente. Este projeto final é o padrão em miniatura. Uma biblioteca de estratégia é o lugar mais fácil para praticar: o sinal transforma preços em posições, o backtest transforma posições em retornos depois de custos, as métricas resumem os retornos e um executor liga tudo. Cada etapa recebe arrays simples e devolve arrays simples, então cada uma pode ser testada sozinha com números que se conferem à mão.
# The shape of a small library: a package, its tests, its metadata and its automated checks
tree = """\
quantlib/
├── pyproject.toml name, version, dependencies (numpy)
├── src/quantlib/
│ ├── __init__.py the public API: sma_positions, strategy_returns, performance_summary, run_strategy
│ ├── signals.py prices -> positions
│ ├── backtest.py positions -> returns after costs
│ └── metrics.py returns -> total return, volatility, Sharpe, drawdown
├── tests/ one test file per module, fixed seeds, hand-checked cases
└── .github/workflows/ci.yml lint, types, tests on every push
"""
print(tree.splitlines()[0]) # quantlib/
print(sum(1 for line in tree.splitlines() if ".py" in line)) # 4 the four module filesSinal e backtest: sem antecipação e com custos
O sinal aqui é um cruzamento de médias móveis: ficar comprado quando a média rápida dos preços está acima da lenta, vendido quando está abaixo, zerado quando são iguais ou a lenta ainda não existe. As duas regras que decidem se um backtest é honesto aparecem no código. Primeiro, sem antecipação: a posição decidida no fechamento do dia t só pode usar preços até o dia t, e ganha o retorno do dia t + 1, por isso o retorno do passo t usa positions[t - 1]. Um teste prova: remover os últimos preços não pode mudar as posições anteriores. Segundo, os custos: cada mudança de posição paga cost * |mudança|, então uma estratégia que vira o tempo todo pode ganhar antes dos custos e perder depois.
# The signal and the honest backtest: positions decided at the close are earned on the NEXT step, and trading costs money
import numpy as np
def sma_positions(prices, fast, slow):
prices = np.asarray(prices, dtype=float)
positions = np.zeros(len(prices), dtype=int)
for t in range(slow - 1, len(prices)):
f = prices[t - fast + 1 : t + 1].mean()
s = prices[t - slow + 1 : t + 1].mean()
positions[t] = 1 if f > s else -1 if f < s else 0
return positions
def strategy_returns(prices, positions, cost):
returns = []
for t in range(1, len(prices)):
before = positions[t - 2] if t >= 2 else 0
returns.append(positions[t - 1] * (prices[t] / prices[t - 1] - 1) - cost * abs(positions[t - 1] - before))
return np.array(returns)
prices = np.array([100, 101, 103, 102, 105, 108, 107, 110, 112, 111, 115, 118], dtype=float)
positions = sma_positions(prices, 2, 4)
print(positions) # [0 0 0 1 1 1 1 1 1 1 1 1] flat until the 4-day average exists, then long
print(np.allclose(sma_positions(prices[:8], 2, 4), positions[:8])) # True later prices never change earlier positions
print(np.round(strategy_returns(prices, positions, 0.0).sum() - strategy_returns(prices, positions, 0.001).sum(), 3)) # 0.001 one purchase at 0.1%Os números de desempenho e o executor
Uma série de retornos se resume em poucos números. O retorno total compõe os períodos, prod(1 + r) - 1; o retorno anual o reescala para um ano, (1 + total)^(períodos_por_ano / n) - 1; a volatilidade anual é o desvio padrão amostral vezes sqrt(períodos_por_ano); o índice de Sharpe divide o retorno médio por esse desvio, anualizado, e é a medida principal de retorno por unidade de risco (0 para uma série plana em vez de um erro de divisão); o drawdown máximo é a pior queda a partir de um pico da curva de capital, o número que diz quanta dor a estratégia pede que você suporte. Uma função executor amarra tudo: posições, depois retornos, depois o resumo. Ela deve apenas compor as outras funções, para que um bug sempre se rastreie até uma etapa.
# Performance statistics of a return series, and the runner that wires the three stages together
import math
import numpy as np
def performance_summary(returns, periods_per_year):
returns = np.asarray(returns, dtype=float)
n = len(returns)
equity = np.concatenate(([1.0], np.cumprod(1 + returns)))
total = float(equity[-1] - 1)
deviation = float(np.std(returns, ddof=1))
peak = np.maximum.accumulate(equity)
return {
"total_return": total,
"annual_return": float((1 + total) ** (periods_per_year / n) - 1),
"annual_vol": deviation * math.sqrt(periods_per_year),
"sharpe": float(returns.mean() / deviation * math.sqrt(periods_per_year)) if deviation > 0 else 0.0,
"max_drawdown": float(np.max((peak - equity) / peak)),
}
summary = performance_summary([0.10, -0.20, 0.05], 252)
print(round(summary["total_return"], 4), round(summary["max_drawdown"], 4)) # -0.076 0.2 equity 1 -> 1.1 -> 0.88 -> 0.924: a 20% fall from the peak
print(performance_summary([0.0, 0.0, 0.0], 252)["sharpe"]) # 0.0 no variation: a Sharpe of 0, not an error# The runner only composes: if a number looks wrong you can test each stage on its own
def run_strategy(prices, fast, slow, cost, periods_per_year=252):
positions = sma_positions(prices, fast, slow)
returns = strategy_returns(prices, positions, cost)
return performance_summary(returns, periods_per_year)
# (sma_positions, strategy_returns and performance_summary are the functions you wrote in this lab)
steps = ["sma_positions", "strategy_returns", "performance_summary"]
print(" -> ".join(steps)) # sma_positions -> strategy_returns -> performance_summary
print(len(steps), "stages, one result: a dict of five numbers") # 3 stages, one result: a dict of five numbersSeu projeto final
Agora construa o seu, no seu próprio repositório. Escolha uma de duas direções. Uma biblioteca de precificação: Black-Scholes e as gregas, um precificador Monte Carlo com variáveis antitéticas, um solucionador de volatilidade implícita e a calibração de um modelo a um conjunto de cotações. Ou uma biblioteca de estratégias: vários sinais, um backtest com custos e limites de posição, avaliação walk-forward e um relatório. Em ambos os casos, entregue as mesmas coisas: um pacote com API pública clara e anotações de tipo; testes com sementes fixas e casos conferidos à mão; um pyproject.toml e um README mostrando como instalar e rodar um exemplo; um workflow de CI com lint, tipos e testes; uma nota de desempenho com um gargalo perfilado; e um texto curto sobre as premissas e o que quebraria o modelo. Divida seu tempo: projete a interface primeiro, construa o menor pipeline que funcione e depois amplie.
- Uma biblioteca são pequenas funções testadas compostas num pipeline atrás de uma interface clara: sinal -> posições -> retornos após custos -> métricas, ligadas por um executor.
- Um backtest honesto não tem antecipação (a posição em t usa preços até t e ganha o retorno seguinte) e cobra custos a cada mudança de posição.
- Resuma com retorno total e anual, volatilidade, Sharpe e drawdown máximo; depois entregue um pacote com testes, CI, um README e um texto honesto.
Isso completa o roteiro. Agora você tem a linguagem, as ferramentas numéricas, as peças de dados de mercado e execução, os modelos e os hábitos de engenharia de um desenvolvedor quant em atividade. Guarde seu repositório final: é a melhor prova das habilidades que você construiu aqui.
Lab: o que é avaliado
- Transformar uma tendência em posições com
sma_positions25 pts - Fazer backtest com custos em
strategy_returns25 pts - Julgar o resultado com
performance_summary25 pts - Ligar tudo em
run_strategy25 pts