σμ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 AcademySkills
Ctrl K
AO
+1,2 Tsd. Lernende
AnmeldenBeitreten ↗
Skills
AO
+1,2 Tsd. Lernende
Anmelden
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Python-Skill›Modul 10 · Hochleistungs-Python-Entwicklung›Thema 10.3·~50 Min.Fortgeschritten

Testen mit pytest

Position in der SkillModul 10 von 11 · Thema 3 von 13
10.3 Testen mit pytest
—Lab-Punkte— / 100XP gesamt—Serie—

Warum testen und wie ein Test aussieht

Im Trading ist ein Bug keine Ausnahme auf dem Bildschirm, sondern eine plausibel aussehende falsche Zahl: ein vertauschtes Vorzeichen im P&L, doppelt angewandte Gebühren, ein Signal, das still den Preis von morgen nutzt. Tests sind kleine Programme, die Ihren Code gegen bekannte Antworten prüfen, sodass so ein Bug auf Ihrem Rechner statt am Markt auffällt. pytest findet Funktionen namens test_*, führt jede aus und meldet die, bei denen ein assert scheitert. Ein Test ist ohne Framework lesbar: Eingabe vorbereiten, Funktion aufrufen, das Erwartete mit assert prüfen.

# 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, Fehler und viele Fälle

Floats sind ungenau, daher ist 0.1 + 0.2 == 0.3 falsch. Vergleichen Sie Zahlen mit Toleranz: pytest.approx(expected) akzeptiert eine kleine relative Abweichung (standardmäßig ein Millionstel, plus eine winzige absolute Untergrenze von 1e-12), und für Werte nahe null, wo eine relative Toleranz nutzlos ist, kann man eine absolute ergänzen. Um zu prüfen, dass schlechte Eingaben abgelehnt werden, besteht pytest.raises(ValueError) nur, wenn diese Ausnahme auftritt, und scheitert, wenn keine (oder eine andere) auftritt. Um einen Test über viele Fälle laufen zu lassen, liefert @pytest.mark.parametrize eine Tabelle aus Eingaben und Erwartungen, meldet jede Zeile einzeln und macht das Hinzufügen eines Falls zu einer einzeiligen Änderung.

# 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, Seeds und was zu testen ist

Eine Fixture ist ein gemeinsamer Aufbau, den Tests beim Namen anfordern: eine kleine Preisreihe, ein Zufallsgenerator mit Seed, eine temporäre Datei. Fixieren (oder drucken) Sie bei Zufallsdaten den Seed, damit sich ein Fehlschlag reproduzieren lässt: Ein Test, der einmal in hundert Läufen scheitert und sich nicht wiederholen lässt, ist schlechter als keiner. Testen Sie das Verhalten, das zählt, nicht die Interna: bekannte Fälle von Hand, Randfälle (leere Eingabe, null, ein Element), Invarianten, die immer gelten müssen (eine Wahrscheinlichkeit liegt zwischen 0 und 1, Kaufen und Verkaufen zum gleichen Preis kostet genau die Gebühren), und einen Regressionstest für jeden behobenen Bug. Lässt sich eine für jede Eingabe geltende Regel formulieren, ist die Suche nach einem Gegenbeispiel unter vielen Zufallseingaben ein billiger, starker Test.

# 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
Ein Test, der nicht scheitern kann, beweist nichtsLassen Sie jeden Test zuerst scheitern, indem Sie den Code absichtlich kaputt machen, damit Sie wissen, dass er den Bug fangen kann, den er fangen soll. Kopieren Sie nicht die Formel der Funktion in den Test: Teilen beide einen Fehler, stimmen sie überein und der Test besteht. Berechnen Sie den erwarteten Wert unabhängig oder von Hand. Und halten Sie Tests schnell und frei von Netz und Uhr, damit Sie sie wirklich vor jeder Änderung ausführen.
  • Ein Test bereitet die Eingabe vor, ruft die Funktion auf und prüft das Ergebnis mit assert; pytest führt jede test_*-Funktion aus und meldet Fehlschläge.
  • Vergleichen Sie Floats mit pytest.approx (nahe null absolute Toleranz ergänzen), erwarten Sie Fehler mit pytest.raises und nutzen Sie parametrize für Falltabellen.
  • Fixieren Sie Zufalls-Seeds, testen Sie Randfälle, Invarianten und behobene Bugs, suchen Sie Gegenbeispiele und lassen Sie jeden Test einmal scheitern, bevor Sie ihm vertrauen.

Als Nächstes: Profiling von Python-Code, messen, wohin die Zeit geht, bevor man ihn schneller macht.

Lab: Was bewertet wird

  • Floats mit Toleranz in is_close vergleichen25 Pkt.
  • Mit run_cases eine Falltabelle ausführen25 Pkt.
  • Mit raises einen Fehler erwarten25 Pkt.
  • Mit find_counterexample einen Fehlschlag aufspüren25 Pkt.
Explorer · lab-119-1
▾ lab-119-1
…
bash · im Browser
learner@quantcraft:~/lab-119-1$
Python noch nicht geladenPythonLn 1, Col 1Spaces: 4UTF-8

Dieses Thema zurücksetzen?

Alles in lab-119-1 wird auf die Startdateien zurückgesetzt und das Terminal geleert. Ihre aktuelle Lösung geht verloren – das lässt sich nicht rückgängig machen.

Wird zurückgesetzt
  • main.py → Startcode
  • Terminal-Verlauf und Testausgabe
  • Nicht eingereichte Änderungen
Bleibt erhalten
  • Beste Lab-Punktzahl
  • Ihre XP und Ihre Serie
  • Vorlesungsfortschritt und Notizen

Melde dich an, um deinen Code auszuführen.

Alle Lektionen kannst du kostenlos lesen. Erstelle ein Konto oder melde dich an, um Code auszuführen, Tests zu starten und dieses Lab abzugeben.

  • IDE im Browser mit Bash-Terminal — nichts zu installieren
  • Sofortige Bewertung, XP und dein Platz auf der Skill
Konto erstellenAnmelden
ODER
Weiter mit GitHub