"""Eigenschafts-Tests (`hypothesis`) auf den REINEN Funktionen (2026-08-20). ⚠⚠ WOZU — die Suite prueft bisher BEISPIELE, und die schreibe ich selbst. Genau dort sitzt der blinde Fleck: ich waehle die Faelle aus, an die ich denke. Die teuersten Fehler dieses Projekts sassen aber auf GRENZEN, an die niemand dachte: · `room_info` (10.08.): `frei` MUSS auf dem UNGERUNDETEN Abstand entschieden werden - bei 0,5951 laesst die auf 0,60 gerundete Zahl den Fall faelschlich durchs Gate. Gegenprobe mit hypothesis: findet `dist=0.59765625` in Sekunden. · `_broker_offset_s` (01.08.): die Plausibilitaetsspanne [-12 h … +14 h] liess eine Wochenend-Veraltung von -9,5 h durch. Ein Property-Test sucht die Grenze selbst und SCHRUMPFT das Gegenbeispiel auf den kleinsten Fall. Bewusst nur auf REINEN Funktionen - alles, was MT5, DB oder Zeit braucht, gehoert in die Szenario-Tests. """ import pytest hypothesis = pytest.importorskip("hypothesis") # dev-only, Suite laeuft auch ohne from hypothesis import given, settings # noqa: E402 from hypothesis import strategies as stg # noqa: E402 from core.sr_levels import cluster # noqa: E402 from core.stichprobe import autokorrelation, entkoppeln # noqa: E402 PREIS = stg.floats(min_value=50.0, max_value=120.0, allow_nan=False, allow_infinity=False) @given(stg.lists(PREIS, min_size=0, max_size=40), stg.floats(min_value=0.01, max_value=1.0, allow_nan=False)) @settings(max_examples=300, deadline=None) def test_cluster_erfindet_und_verliert_nichts(preise, tol): """Ein Cluster-Ergebnis muss IMMER innerhalb der Eingabespanne liegen und darf nie mehr Linien haben als Eingaben.""" out = cluster(preise, tol) assert len(out) <= max(1, len(preise)) or not preise if preise and out: # ⚠ Toleranz = eine halbe Rundungsstufe. `cluster` rundet jede Linie # auf 3 Stellen (wie `_draw_levels`), das Ergebnis kann also knapp # ausserhalb der Eingabespanne liegen. hypothesis hat genau das # gefunden (50.453125 -> 50.453) - meine erste Invariante war zu # streng, der Code ist richtig. Genau dafuer sind Property-Tests da: # sie zeigen, wo eine Annahme nicht traegt. assert min(out) >= min(preise) - 0.0005 - 1e-9 assert max(out) <= max(preise) + 0.0005 + 1e-9 @given(stg.lists(PREIS, min_size=1, max_size=30), stg.floats(min_value=0.01, max_value=1.0, allow_nan=False)) @settings(max_examples=300, deadline=None) def test_cluster_ist_sortiert_und_ohne_duplikate(preise, tol): out = cluster(preise, tol) assert out == sorted(out) assert len(out) == len(set(out)) @given(stg.lists(stg.tuples(stg.integers(0, 10_000), stg.integers(0, 5)), min_size=0, max_size=200)) @settings(max_examples=300, deadline=None) def test_entkoppeln_behaelt_je_schluessel_genau_eine_zeile(paare): """Invariante: nach dem Entkoppeln ist JEDER Schluessel genau einmal da — und die Reihenfolge der behaltenen Zeilen bleibt erhalten.""" zeilen = [{"ts": t, "k": k} for t, k in paare] out = entkoppeln(zeilen, lambda z: z["k"]) ks = [z["k"] for z in out] assert len(ks) == len(set(ks)) assert set(ks) == {k for _, k in paare} # ⚠ Reihenfolge ueber die IDENTITAET pruefen, nicht ueber Gleichheit: # zwei inhaltsgleiche dicts (z. B. zweimal {"ts":0,"k":0}) sind `==`, und # `z in out` faende dann beide. Auch das hat hypothesis aufgedeckt - mein # Test war falsch, nicht `entkoppeln`. behalten = {id(z) for z in out} assert [id(z) for z in out] == [id(z) for z in zeilen if id(z) in behalten] @given(stg.lists(stg.floats(min_value=-5.0, max_value=5.0, allow_nan=False), min_size=3, max_size=500)) @settings(max_examples=300, deadline=None) def test_autokorrelation_bleibt_im_gueltigen_band(x): """Eine Korrelation liegt zwischen -1 und 1 — auch bei konstanten Reihen, wo der Nenner 0 wird (dort ist 0 definiert).""" r = autokorrelation(x) assert -1.0 <= r <= 1.0