σμ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.6·~1 hAvançado

Numba

Posição no roteiroMódulo 10 de 11 · Tópico 6 de 13
10.6 Numba
—Nota do lab— / 100XP total—Sequência—

Compilar os laços que você escreveu

Alguns cálculos são naturalmente um laço: cada passo depende do anterior, como numa média ponderada exponencialmente, num drawdown ou no payoff de uma opção dependente do caminho. O NumPy não consegue vetorizá-los, e um laço Python sobre um milhão de elementos é lento porque cada passo passa pelo interpretador. O Numba é um compilador just-in-time: você coloca @njit sobre uma função e, na primeira chamada, o Numba traduz o laço Python em código de máquina para os tipos exatos recebidos. As chamadas seguintes rodam em velocidade próxima à do C, muitas vezes cinquenta a cem vezes mais rápido que o mesmo laço em Python puro, sem mudar o código.

# An exponentially weighted average is a loop: each value depends on the one before
import numpy as np

try:
    from numba import njit                           # one decorator compiles the function when it is installed
except ImportError:
    def njit(func):                                  # without Numba the same code simply runs as Python
        return func

@njit
def ewma(values, alpha):
    out = np.empty(len(values))
    out[0] = values[0]
    for i in range(1, len(values)):
        out[i] = alpha * values[i] + (1.0 - alpha) * out[i - 1]
    return out

print(ewma(np.array([1.0, 2.0, 3.0]), 0.5))          # [1.   1.5  2.25]

O estilo de que o Numba gosta

No modo nopython, que o @njit seleciona, o Numba compila apenas um subconjunto do Python e recusa o resto em vez de recair silenciosamente em código lento. Ele entende números, arrays do NumPy, tuplas, laços simples, if e grande parte das funções do NumPy. Não entende pandas, objetos Python arbitrários nem dicionários de tipos mistos. Então escreva a função crítica no estilo simples: receba arrays e escalares, aloque a saída com np.empty ou np.zeros, itere com for i in range(n) e mantenha acumuladores escalares (total, peak). Chame-a sempre com os mesmos tipos de argumentos, pois uma nova combinação de tipos dispara uma nova compilação.

# A drawdown is a running peak and a running worst case: two scalars and one loop
import numpy as np

try:
    from numba import njit
except ImportError:
    def njit(func):
        return func

@njit
def max_drawdown(equity):
    peak = 0.0
    worst = 0.0
    for i in range(len(equity)):
        value = equity[i]
        if value > peak:
            peak = value
        drawdown = (peak - value) / peak
        if drawdown > worst:
            worst = drawdown
    return worst

curve = np.array([100.0, 120.0, 90.0, 130.0, 117.0])
print(max_drawdown(curve))                           # 0.25   from 120 down to 90
print(max_drawdown(np.array([1.0, 2.0, 3.0])))       # 0.0    a curve that only rises

Tempo de compilação, cache e onde o Numba não ajuda

A primeira chamada paga a compilação, muitas vezes um segundo ou mais, então cronometre uma função Numba depois de chamá-la uma vez com poucos dados, e use @njit(cache=True) para guardar o código compilado entre execuções. O ganho exige trabalho suficiente: uma função chamada um milhão de vezes com três números gasta o tempo no custo da chamada, não no laço. O Numba ajuda em laços sobre arrays; não faz nada por código que já é uma chamada NumPy, por esperar um banco de dados nem por um mau algoritmo. @njit(parallel=True) com prange pode espalhar iterações independentes pelos núcleos, e fastmath=True troca um pouco do rigor de ponto flutuante por velocidade, o que só deve ser ativado após verificar que os resultados seguem dentro da tolerância.

# Counting crossings: Numba compiles any loop like this, so keep the logic in scalars
import numpy as np

try:
    from numba import njit
except ImportError:
    def njit(func):
        return func

@njit(cache=False)
def count_crossovers(fast, slow):
    count = 0
    previous = 0.0
    for i in range(len(fast)):
        gap = fast[i] - slow[i]
        if i > 0 and previous * gap < 0:
            count += 1
        previous = gap
    return count

fast = np.array([1.0, 3.0, 2.0, 1.0, 4.0])
slow = np.full(5, 2.0)
print(count_crossovers(fast, slow))                  # 2     below, above, touch, below, above: two strict sign changes
# Options change how a function is compiled; here they are ignored if Numba is not installed
import numpy as np

try:
    from numba import njit, prange
except ImportError:
    prange = range

    def njit(*args, **kwargs):
        if args and callable(args[0]):
            return args[0]
        return lambda func: func

@njit(cache=True)                                    # keep the compiled code on disk between runs
def total_notional(prices, quantities):
    total = 0.0
    for i in prange(len(prices)):                    # prange runs in parallel only with parallel=True
        total += prices[i] * quantities[i]
    return total

prices = np.array([184.25, 410.5, 99.0])
quantities = np.array([100.0, 50.0, 200.0])
print(total_notional(prices, quantities))            # 58750.0
print(total_notional(prices, quantities) == float(prices @ quantities))     # True  same answer as the vectorised dot product
Verifique as respostas, não só a velocidadeUm laço compilado pode errar de formas novas: estouro de inteiros em vez dos inteiros ilimitados do Python, um array float32 usado em silêncio onde você esperava float64, um índice fora dos limites que lê lixo em vez de levantar um erro. Compare o resultado compilado com a versão Python simples em entradas aleatórias (com fn.py_func, que o Numba mantém), como fazem os testes destes laboratórios, e ative a verificação de limites durante o desenvolvimento.
  • @njit compila uma função Python em código de máquina na primeira chamada; laços que dependem de passos anteriores ficam rápidos sem vetorizar.
  • Escreva para o modo nopython: arrays e escalares, saída pré-alocada, for i in range(n), acumuladores escalares; sem pandas nem contêineres de tipos mistos.
  • Cronometre após uma chamada de aquecimento, use cache=True, não espere ganho em código que já é uma chamada NumPy e confira os resultados com a versão Python simples.

A seguir: Cython, outra forma de compilar laços Python, adicionando tipos e construindo um módulo de extensão.

Lab: o que é avaliado

  • Manter um total corrente em rolling_mean25 pts
  • Acompanhar o pico em max_drawdown25 pts
  • Contar os cruzamentos em count_crossovers25 pts
  • Preencher um array de saída em ewma25 pts
Explorador · lab-122-1
▾ lab-122-1
…
bash · no navegador
learner@quantcraft:~/lab-122-1$
Python ainda não carregadoPythonLn 1, Col 1Spaces: 4UTF-8

Redefinir este tópico?

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