σμ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 10 · Engenharia Python de alto desempenho›Tópico 10.3·~50 minAvançado

Testes com pytest

Posição no roteiroMódulo 10 de 11 · Tópico 3 de 13
10.3 Testes com pytest
—Nota do lab— / 100XP total—Sequência—

Por que testar e como é um teste

No trading, um bug não é uma exceção na tela; é um número errado que parece plausível: um sinal trocado num P&L, uma taxa aplicada duas vezes, um sinal que usa em silêncio o preço de amanhã. Testes são pequenos programas que verificam seu código contra respostas conhecidas, de modo que esse bug é pego na sua máquina e não no mercado. O pytest encontra funções chamadas test_*, executa cada uma e informa as em que um assert falha. Um teste se lê sem framework: prepare uma entrada, chame a função e faça assert do que espera.

# A test is a function with an assert; pytest runs every test_* function it finds
def net_pnl(entry, exit_price, quantity, fee):
    return (exit_price - entry) * quantity - fee

def test_net_pnl_long_winner():
    assert net_pnl(100.0, 102.0, 50, 1.0) == 99.0

def test_net_pnl_short_loser():
    assert net_pnl(100.0, 102.0, -50, 1.0) == -101.0

for test in (test_net_pnl_long_winner, test_net_pnl_short_loser):
    test()                                           # pytest does this for you and reports failures
    print(test.__name__, "passed")

Floats, erros e muitos casos

Floats são aproximados, então 0.1 + 0.2 == 0.3 é falso. Compare números com tolerância: pytest.approx(expected) aceita uma pequena diferença relativa (por padrão uma parte em um milhão, mais um piso absoluto minúsculo de 1e-12) e dá para somar uma tolerância absoluta para valores próximos de zero, onde a relativa não serve. Para verificar que uma entrada ruim é rejeitada, pytest.raises(ValueError) só passa se essa exceção ocorrer, e falha se nenhuma (ou outra) ocorrer. Para rodar um teste com muitos casos, @pytest.mark.parametrize fornece uma tabela de entradas e valores esperados, informa cada linha separadamente e faz de adicionar um caso uma mudança de uma linha.

# Floats need a tolerance: approx accepts a tiny relative difference, == does not
import pytest

print(0.1 + 0.2 == 0.3)                              # False
print(0.1 + 0.2 == pytest.approx(0.3))               # True
print(1e-10 == pytest.approx(0.0))                   # False  near zero a relative tolerance cannot help
print(1e-10 == pytest.approx(0.0, abs=1e-9))         # True   a larger absolute tolerance does
# raises: the test passes only if the expected exception happens
import pytest

def leverage(gross, equity):
    if equity <= 0:
        raise ValueError("equity must be positive")
    return gross / equity

with pytest.raises(ValueError) as info:
    leverage(1000.0, 0.0)
print("raised:", info.value)                         # raised: equity must be positive

try:
    with pytest.raises(ValueError):
        leverage(1000.0, 500.0)                      # no exception: pytest.raises itself fails
except BaseException as outcome:
    print(type(outcome).__name__)                    # Failed
# parametrize in miniature: one function, a table of cases, each row judged on its own
def fee(notional, rate=0.001, minimum=1.0):
    return max(notional * rate, minimum)

cases = [
    ((100_000.0,), 100.0),
    ((500.0,), 1.0),                                 # the minimum fee applies
    ((2_000.0, 0.002), 4.0),
    ((0.0,), 1.0),
]
for index, (args, expected) in enumerate(cases):
    got = fee(*args)
    print(index, "ok" if abs(got - expected) <= 1e-9 * abs(expected) else f"FAIL got {got}")

Fixtures, sementes e o que testar

Uma fixture é uma preparação compartilhada que os testes pedem pelo nome: uma pequena série de preços, um gerador aleatório com semente, um arquivo temporário. Com dados aleatórios, fixe a semente (ou imprima-a) para poder reproduzir uma falha: um teste que falha uma vez em cem e não pode ser repetido é pior que nenhum. Teste o comportamento que importa e não os detalhes internos: casos conhecidos à mão, casos-limite (entrada vazia, zero, um elemento), invariantes que sempre valem (uma probabilidade está entre 0 e 1, comprar e vender ao mesmo preço perde exatamente as taxas) e um teste de regressão para cada bug corrigido. Quando se consegue enunciar uma regra válida para toda entrada, procurar um contraexemplo entre muitas entradas aleatórias é um teste barato e poderoso.

# A seed makes a random test replayable; a property is searched for a counterexample
import random

def find_counterexample(prop, inputs):
    for value in inputs:
        if not prop(value):
            return value
    return None

rng = random.Random(42)                              # fixed seed: the same inputs on every run
returns = [round(rng.uniform(-0.05, 0.05), 4) for _ in range(1000)]

def compound_is_positive_growth(r):
    return (1 + r) > 0                               # a daily return above -100% keeps the value positive

print(find_counterexample(compound_is_positive_growth, returns))      # None  the rule holds for all 1000
print(find_counterexample(lambda r: r > -0.049, returns))             # a return that breaks a wrong rule
Um teste que não pode falhar não prova nadaFaça cada teste falhar primeiro, quebrando o código de propósito, para saber que ele consegue pegar o bug que deve pegar. Não copie a fórmula da própria função para o teste: se as duas compartilham um erro, concordam e o teste passa. Calcule o valor esperado de forma independente ou à mão. E mantenha os testes rápidos e sem rede nem relógio, para realmente rodá-los antes de cada mudança.
  • Um teste prepara a entrada, chama a função e faz assert do resultado; o pytest executa cada função test_* e informa as falhas.
  • Compare floats com pytest.approx (some tolerância absoluta perto de zero), espere erros com pytest.raises e use parametrize para tabelas de casos.
  • Fixe as sementes, teste casos-limite, invariantes e bugs corrigidos, procure contraexemplos e faça cada teste falhar uma vez antes de confiar nele.

A seguir: profiling de código Python, medir para onde vai o tempo antes de tentar acelerá-lo.

Lab: o que é avaliado

  • Comparar floats com tolerância em is_close25 pts
  • Executar uma tabela de casos com run_cases25 pts
  • Esperar um erro com raises25 pts
  • Caçar uma falha com find_counterexample25 pts
Explorador · lab-119-1
▾ lab-119-1
…
bash · no navegador
learner@quantcraft:~/lab-119-1$
Python ainda não carregadoPythonLn 1, Col 1Spaces: 4UTF-8

Redefinir este tópico?

Tudo em lab-119-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