σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademyRoteiros
Ctrl K
AO
+1,2 mil alunos
EntrarParticipar ↗
Roteiros
AO
+1,2 mil alunos
Entrar
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Roteiro de Python›Módulo 6 · Backtesting e custos de trading›Tópico 6.2·~1 h 15 minAvançado

Um backtester orientado a eventos

Posição no roteiroMódulo 6 de 11 · Tópico 2 de 6
6.2 Um backtester orientado a eventos
—Nota do lab— / 100XP total—Sequência—

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 turned

As 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 0

O 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))
O mesmo código pode ir ao vivoO laço vê uma barra e age: é exatamente o formato de um programa de trading ao vivo, em que a próxima barra chega de um feed de dados e não de uma lista. Separar sinal, ordem e conta em funções distintas permite trocar o feed histórico por um ao vivo sem reescrever a estratégia.
  • Um backtester por eventos percorre as barras com estado (caixa, ações); na barra t só pode usar prices[: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 k barras deve igualar os primeiros k resultados 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
Explorador · lab-88-1
▾ lab-88-1
…
bash · no navegador
learner@quantcraft:~/lab-88-1$
Python ainda não carregadoPythonLn 1, Col 1Spaces: 4UTF-8

Redefinir este tópico?

Tudo em lab-88-1 volta aos arquivos iniciais e o terminal é limpo. Sua solução atual será perdida — isso não pode ser desfeito.

Será redefinido
  • main.py → código inicial
  • Histórico do terminal e saída dos testes
  • Alterações não enviadas
Será mantido
  • Melhor nota do lab
  • Seu XP e sua sequência
  • Progresso da aula e anotações

Entre para executar o seu código.

Todas as aulas são gratuitas para ler. Crie uma conta ou entre para executar código, rodar os testes e enviar este lab.

  • IDE no navegador com terminal bash — nada para instalar
  • Correção instantânea, XP e o seu lugar no roteiro
Criar uma contaEntrar
OU
Continuar com o GitHub