σμ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.3·~50 minAvancé

Tester avec pytest

Position dans le parcoursModule 10 sur 11 · Sujet 3 sur 13
10.3 Tester avec pytest
—Score du lab— / 100XP total—Série—

Pourquoi tester, et à quoi ressemble un test

En trading, un bug n'est pas une exception sur un écran ; c'est un nombre faux mais plausible : un signe inversé dans un P&L, des frais appliqués deux fois, un signal qui utilise discrètement le prix de demain. Les tests sont de petits programmes qui vérifient votre code contre des réponses connues, de sorte qu'un tel bug est attrapé sur votre machine et non sur le marché. pytest trouve les fonctions nommées test_*, exécute chacune et signale celles où un assert échoue. Un test se lit sans framework : préparez une entrée, appelez la fonction, faites un assert de ce que vous attendez.

# 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")

Flottants, erreurs et nombreux cas

Les flottants sont approximatifs, donc 0.1 + 0.2 == 0.3 est faux. Comparez les nombres avec une tolérance : pytest.approx(expected) accepte une petite différence relative (par défaut une partie par million, plus un plancher absolu minuscule de 1e-12), et l'on peut ajouter une tolérance absolue pour les valeurs proches de zéro, où une tolérance relative est inutile. Pour vérifier qu'une mauvaise entrée est rejetée, pytest.raises(ValueError) ne passe que si cette exception se produit, et échoue s'il n'y en a aucune (ou une autre). Pour exécuter un test sur de nombreux cas, @pytest.mark.parametrize fournit un tableau d'entrées et de valeurs attendues, rapporte chaque ligne séparément et fait d'un ajout de cas un changement d'une ligne.

# 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, graines et quoi tester

Une fixture est une préparation partagée que les tests demandent par leur nom : une petite série de prix, un générateur aléatoire à graine fixée, un fichier temporaire. Avec des données aléatoires, fixez la graine (ou affichez-la) pour qu'un échec soit reproductible : un test qui échoue une fois sur cent et ne peut pas être rejoué est pire que rien. Testez le comportement qui compte plutôt que les détails internes : des cas connus à la main, des cas limites (entrée vide, zéro, un seul élément), des invariants qui doivent toujours tenir (une probabilité est entre 0 et 1, acheter puis vendre au même prix perd exactement les frais), et un test de non-régression pour chaque bug corrigé. Quand on peut énoncer une règle valable pour toute entrée, chercher un contre-exemple parmi de nombreuses entrées aléatoires est un test peu coûteux et puissant.

# 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
Un test qui ne peut pas échouer ne prouve rienFaites d'abord échouer chaque test, en cassant volontairement le code, pour savoir qu'il peut attraper le bug visé. Ne recopiez pas la formule de la fonction dans le test : si les deux partagent une erreur, elles concordent et le test passe. Calculez la valeur attendue de façon indépendante ou à la main. Et gardez des tests rapides, sans réseau ni horloge, pour les lancer réellement avant chaque changement.
  • Un test prépare l'entrée, appelle la fonction et fait un assert du résultat ; pytest exécute chaque fonction test_* et rapporte les échecs.
  • Comparez les flottants avec pytest.approx (ajoutez une tolérance absolue près de zéro), attendez les erreurs avec pytest.raises et utilisez parametrize pour les tables de cas.
  • Fixez les graines aléatoires, testez cas limites, invariants et bugs corrigés, cherchez des contre-exemples, et faites échouer chaque test une fois avant de lui faire confiance.

À suivre : le profilage du code Python, mesurer où passe le temps avant d'essayer de l'accélérer.

Lab : ce qui est noté

  • Comparer des flottants avec une tolérance dans is_close25 pts
  • Exécuter une table de cas avec run_cases25 pts
  • S'attendre à une erreur avec raises25 pts
  • Chasser une défaillance avec find_counterexample25 pts
Explorateur · lab-119-1
▾ lab-119-1
…
bash · dans le navigateur
learner@quantcraft:~/lab-119-1$
Python pas encore chargéPythonLn 1, Col 1Spaces: 4UTF-8

Réinitialiser ce sujet ?

Tout le contenu de lab-119-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