σμ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 AcademyParcours
Ctrl K
AO
+1,2 k apprenants
ConnexionRejoindre ↗
Parcours
AO
+1,2 k apprenants
Connexion
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Parcours Python›Module 10 · Ingénierie Python haute performance›Sujet 10.6·~1 hAvancé

Numba

Position dans le parcoursModule 10 sur 11 · Sujet 6 sur 13
10.6 Numba
—Score du lab— / 100XP total—Série—

Compiler les boucles que vous avez écrites

Certains calculs sont naturellement une boucle : chaque étape dépend de la précédente, comme dans une moyenne pondérée exponentiellement, un drawdown ou le gain d'une option dépendant du chemin. NumPy ne sait pas vectoriser cela, et une boucle Python sur un million d'éléments est lente car chaque étape passe par l'interpréteur. Numba est un compilateur à la volée : vous mettez @njit au-dessus d'une fonction, et au premier appel Numba traduit la boucle Python en code machine pour les types exacts reçus. Les appels suivants tournent à peu près à la vitesse du C, souvent cinquante à cent fois plus vite que la même boucle en Python pur, sans changer le code.

# 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]

Le style qu'aime Numba

En mode nopython, que sélectionne @njit, Numba ne compile qu'un sous-ensemble de Python et refuse le reste plutôt que de retomber silencieusement sur du code lent. Il comprend les nombres, les tableaux NumPy, les tuples, les boucles simples, if et une grande part des fonctions de NumPy. Il ne comprend ni pandas, ni les objets Python quelconques, ni les dictionnaires de types mélangés. Écrivez donc la fonction critique dans le style simple : prendre des tableaux et des scalaires, allouer la sortie avec np.empty ou np.zeros, boucler avec for i in range(n) et garder des accumulateurs scalaires (total, peak). Appelez-la avec les mêmes types d'arguments à chaque fois, car une nouvelle combinaison de types déclenche une nouvelle compilation.

# 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

Temps de compilation, cache et limites de Numba

Le premier appel paie la compilation, souvent une seconde ou plus : chronométrez donc une fonction Numba après l'avoir appelée une fois sur de petites données, et utilisez @njit(cache=True) pour garder le code compilé d'une exécution à l'autre. L'accélération demande assez de travail : une fonction appelée un million de fois sur trois nombres passe son temps dans le surcoût d'appel, pas dans la boucle. Numba aide pour les boucles sur des tableaux ; il ne fait rien pour du code qui est déjà un appel NumPy, pour l'attente d'une base de données ou pour un mauvais algorithme. @njit(parallel=True) avec prange peut répartir des itérations indépendantes sur les cœurs, et fastmath=True échange un peu de rigueur flottante contre de la vitesse, à n'activer qu'après avoir vérifié que les résultats restent dans la tolérance.

# 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
Vérifiez les réponses, pas seulement la vitesseUne boucle compilée peut être fausse de façons nouvelles : dépassement d'entier au lieu des entiers illimités de Python, tableau float32 utilisé sans bruit là où l'on attendait du float64, indice hors limites qui lit n'importe quoi au lieu de lever une erreur. Comparez le résultat compilé à la version Python simple sur des entrées aléatoires (avec fn.py_func, que Numba conserve), comme le font les tests de ces labos, et activez la vérification des bornes pendant le développement.
  • @njit compile une fonction Python en code machine au premier appel ; les boucles qui dépendent des étapes précédentes deviennent rapides sans vectorisation.
  • Écrivez pour le mode nopython : tableaux et scalaires, sortie préallouée, for i in range(n), accumulateurs scalaires ; ni pandas ni conteneurs de types mélangés.
  • Chronométrez après un appel d'échauffement, utilisez cache=True, n'attendez aucun gain sur du code déjà réduit à un appel NumPy, et vérifiez les résultats contre la version Python simple.

À suivre : Cython, une autre façon de compiler des boucles Python, en ajoutant des types et en construisant un module d'extension.

Lab : ce qui est noté

  • Tenir un total courant dans rolling_mean25 pts
  • Suivre le sommet dans max_drawdown25 pts
  • Compter les croisements dans count_crossovers25 pts
  • Remplir un tableau de sortie dans ewma25 pts
Explorateur · lab-122-1
▾ lab-122-1
…
bash · dans le navigateur
learner@quantcraft:~/lab-122-1$
Python pas encore chargéPythonLn 1, Col 1Spaces: 4UTF-8

Réinitialiser ce sujet ?

Tout le contenu de lab-122-1 revient aux fichiers de départ et le terminal est vidé. Votre solution actuelle sera perdue : cette action est irréversible.

Sera réinitialisé
  • main.py → code de départ
  • Historique du terminal et sortie des tests
  • Modifications non soumises
Sera conservé
  • Meilleur score du lab
  • Votre XP et votre série
  • Progression du cours et notes

Connectez-vous pour exécuter votre code.

Tous les cours sont gratuits à lire. Créez un compte ou connectez-vous pour exécuter du code, lancer les tests et soumettre ce lab.

  • IDE dans le navigateur avec terminal bash — rien à installer
  • Correction instantanée, XP et votre place dans le parcours
Créer un compteSe connecter
OU
Continuer avec GitHub