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 bereitet die Eingabe vor, ruft die Funktion auf und prüft das Ergebnis mit
assert; pytest führt jedetest_*-Funktion aus und meldet Fehlschläge. - Vergleichen Sie Floats mit
pytest.approx(nahe null absolute Toleranz ergänzen), erwarten Sie Fehler mitpytest.raisesund nutzen Sieparametrizefü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_closevergleichen25 Pkt. - Mit
run_caseseine Falltabelle ausführen25 Pkt. - Mit
raiseseinen Fehler erwarten25 Pkt. - Mit
find_counterexampleeinen Fehlschlag aufspüren25 Pkt.