Property-Based Testing: Bugs finden, die Sie nicht erwartet haben.
Example-Based-Tests prüfen, was Sie sich beim Schreiben vorgestellt haben. Property-Based-Tests prüfen das, was Ihnen nicht eingefallen ist — und genau dort sitzen die Bugs, die ein Live-System um drei Uhr morgens kippen.
Was Property-Based Testing eigentlich ist.
Klassische Unit-Tests sind beispielbasiert: Sie schreiben einen konkreten Input, einen konkreten erwarteten Output, und prüfen Gleichheit. Das ist gut für Regressions-Sicherung, aber schlecht, um neue Bugs zu finden — denn jedes Beispiel ist genau eines, das Sie sich vorstellen konnten.
Property-Based Testing dreht den Spieß um: Sie definieren Invarianten, die für jeden plausiblen Input gelten müssen, und das Framework generiert hunderte oder tausende zufällige Inputs gegen diese Invarianten. Wenn ein Input die Invariante bricht, „shrinkt" das Framework den Input automatisch auf den kleinstmöglichen Fall, der den Bug noch reproduziert.
Hypothesis als Standard-Tool in Python.
Im Python-Ökosystem ist hypothesis der De-facto-Standard. Saubere
Integration mit pytest, exzellentes Shrinking, riesige Bibliothek an Strategien
(Strategies) zur Input-Generierung. Ein minimales Setup:
from hypothesis import given, strategies as st
@given(
equity=st.floats(min_value=1_000, max_value=10_000_000,
allow_nan=False, allow_infinity=False),
risk_pct=st.floats(min_value=0.001, max_value=0.05,
allow_nan=False, allow_infinity=False),
stop_distance=st.floats(min_value=0.01, max_value=100.0,
allow_nan=False, allow_infinity=False),
)
def test_position_size_never_negative(equity, risk_pct, stop_distance):
size = position_size(equity, risk_pct, stop_distance)
assert size >= 0
assert size * stop_distance <= equity * risk_pct * 1.0001
Hypothesis probiert hier hunderte Kombinationen — inklusive Randwerten wie
0.001, sehr kleinen Stop-Distanzen und Extrem-Equity. Wenn die
Funktion irgendwo eine negative Größe zurückgibt, sehen Sie genau die minimale
Eingabe, die das auslöst.
Konkrete Properties für Trading-Code.
Position-Sizing nie negativ, nie über Risiko-Budget
Das obige Beispiel deckt zwei Invarianten ab: Größe ist nie negativ, und das erwartete Verlustpotenzial bei Stop-Out überschreitet nie das definierte Risiko-Budget (plus minimale Floating-Point-Toleranz).
Stop-Loss immer in Trade-Richtung
@given(
entry=st.floats(min_value=1.0, max_value=100_000.0, allow_nan=False),
atr=st.floats(min_value=0.01, max_value=5_000.0, allow_nan=False),
side=st.sampled_from(['long', 'short']),
)
def test_stop_loss_direction(entry, atr, side):
stop = compute_stop(entry, atr, side)
if side == 'long':
assert stop < entry
else:
assert stop > entry
Trivial in der Aussage, aber genau hier sitzt einer der häufigsten Bugs nach Refactorings: Vorzeichen-Flip in der Stop-Berechnung. Ein einziger Property-Test fängt das zuverlässig.
Buy + Sell gleicher Größe = Netto-Null
@given(
qty=st.floats(min_value=0.0001, max_value=1_000_000.0, allow_nan=False),
price_buy=st.floats(min_value=0.01, max_value=100_000.0, allow_nan=False),
price_sell=st.floats(min_value=0.01, max_value=100_000.0, allow_nan=False),
)
def test_round_trip_position_zero(qty, price_buy, price_sell):
book = PositionBook()
book.fill('AAPL', side='buy', qty=qty, price=price_buy)
book.fill('AAPL', side='sell', qty=qty, price=price_sell)
assert book.position('AAPL') == 0
Diese Invariante hat bei mir bereits zweimal Float-Akkumulationsfehler aufgedeckt, die bei beispielbasierten Tests durchgerutscht waren — schlicht, weil ich nie zufällig die exakte Konstellation getroffen hatte.
Backtest-Returns konsistent bei Daten-Reordering
Ein Backtest, der dieselben Trades chronologisch korrekt verarbeitet, sollte für denselben Datensatz unabhängig von der Eingangs-Sortierung des DataFrames dasselbe Ergebnis liefern — Sortierung passiert intern, oder sollte zumindest:
@given(seed=st.integers(min_value=0, max_value=10_000))
def test_backtest_reorder_invariance(seed):
bars = load_sample_bars() # deterministische Fixture
shuffled = bars.sample(frac=1.0, random_state=seed)
r1 = run_backtest(bars)
r2 = run_backtest(shuffled)
assert abs(r1.total_return - r2.total_return) < 1e-9
Bugs, die Property-Tests bei mir gefunden haben.
- NaN-Propagation: Eine Indikator-Funktion gab bei genau einem NaN-Input in der ATR-Berechnung still NaN zurück — und das Position-Sizing multiplizierte fröhlich weiter. Hypothesis fand mit
allow_nan=Truein unter einer Sekunde den minimalen Reproducer. - Off-by-One im Bar-Index: Beim Übergang von Daily- auf Intraday-Bars griff die Backtest-Engine bei der ersten Bar des Tages auf den Schlusskurs des Vortages zu — aber nur bei Symbolen, deren erste Bar im Datensatz nicht ein Montag war. Property-Test mit zufälligen Start-Wochentagen: Sofort gefunden.
- Numerische Stabilität bei kleinen Preisen: Eine Sharpe-Berechnung explodierte bei Penny-Stocks unter 0,01 €, weil die Division durch eine sehr kleine Volatilität zu Infinity-Werten führte. Hypothesis triggerte den Edge-Case mit Preisen knapp über null, die in meinen Beispiel-Tests nie vorkamen.
Strategies sinnvoll einschränken.
Property-Based Testing ist nur so gut wie die Strategy. Wenn Sie
st.floats() ohne Bounds verwenden, generiert Hypothesis auch
1e308, NaN und negative Null — und Ihr Test scheitert an
Inputs, die in der Realität nie vorkommen. Realistische Bounds (Preise zwischen
0,01 und 100 000, Mengen zwischen 1e-4 und 1e6, ATR zwischen 0,01 und einigen
Prozent des Preises) machen die Tests deutlich nützlicher.
Für komplexe Datenstrukturen lohnt sich st.composite: Sie bauen aus
Primitiv-Strategies eine fachlich konsistente Strategy (z. B. einen plausiblen
OHLC-Bar, in dem low ≤ open, close ≤ high immer gilt).
Meine Praxis: kritische Berechnungen, nicht jede Funktion.
Property-Based Testing ist kein Ersatz für klassische Unit- oder Integrations-Tests — sondern ein gezielter Zusatz. Bei den Mandanten, mit denen ich arbeite, setze ich Property-Tests konsequent für drei Bereiche ein:
- Risk-Management: Position-Sizing, Stop-Berechnung, Margin-Checks.
- Order-Routing: Buy-Sell-Round-Trips, Aggregation von Partial-Fills, Konsistenz der internen Position mit Broker-Bestätigungen.
- Backtest-Engine: Reordering-Invarianz, Determinismus bei gleichem Seed, Konsistenz zwischen Vector- und Event-basierter Auswertung.
Für reine UI-Logik, einfache Daten-Transformationen oder I/O-Wrapper sind klassische Tests schneller geschrieben und gut genug. Die Faustregel: Wo ein Bug echtes Geld kostet, lohnt sich der Aufwand für Properties. Wo nur ein Log-Eintrag falsch wäre, reicht ein Beispiel.
Sie wollen kritische Berechnungen in Ihrer Trading-Codebasis robust absichern? Erstgespräch buchen — wir identifizieren die Hotspots und bauen einen schlanken Property-Test-Layer auf.