Nicht nach Katalog beantwortet, sondern gegen die dokumentierten Schwachstellen des Projekts - und beide Kandidaten wurden GEMESSEN, nicht empfohlen. ERTRAG 1 - BLOCK-BOOTSTRAP (core/stichprobe.block_ci95), braucht GAR KEINE Bibliothek. Die Luecke stand seit dem 07.08. woertlich in backtest_cost_gate: "fuer ueberlappende Beobachtungen zu optimistisch ... dafuer braeuchte es einen Block-Bootstrap". Behelf war entkoppeln(). Auf echten Daten (pbreak_predictions): naiv, alle Zeilen n=4702 Breite 0,028 <- 3x zu eng entkoppelt (bisher) n= 552 Breite 0,083 <- 88 % Datenverlust Block L=30 n=4702 Breite 0,081 <- gleiche Breite, ALLE Daten Und das Entkoppeln kostet nicht nur Genauigkeit, es VERSCHIEBT die Schaetzung (0,399 gegen 0,427) - es nimmt systematisch die ERSTE Beruehrung je Level+Stunde. KALIBRIERUNG GEMESSEN (AR(1), bekanntes Mittel, 200 Laeufe, Soll 95 %): rho naiv L=10 L=30 L=60 0,00 94 % 94 % 91 % 92 % 0,50 76 % 92 % 91 % 91 % 0,90 40 % 80 % 89 % 90 % 0,97 18 % 55 % 74 % 83 % Die naive Zeile ist der Befund: bei rho=0,97 behauptet sie 95 % Sicherheit und liegt in 18 % der Faelle richtig. Ehrlich zu den Grenzen: bei unabhaengigen Daten kostet der Block ~3 Punkte (dort bleibt ci95 besser), und bei rho=0,97 deckt auch er nur 83 % - eine Verbesserung, keine Garantie. ERTRAG 2 - hypothesis (dev-only), aufgenommen NACH einer Gegenprobe. Die Suite prueft bisher BEISPIELE, und die waehle ich selbst aus. Die teuersten Fehler sassen auf GRENZEN: room_info musste `frei` auf dem UNGERUNDETEN Abstand entscheiden, weil 0,5951 gerundet durchs 0,6-Gate rutscht. hypothesis findet genau diesen Fall (dist=0.59765625) in Sekunden. Erst danach aufgenommen. Neu tests/test_invarianten.py (4 Eigenschafts-Tests auf REINEN Funktionen), per importorskip - die Suite laeuft auch ohne. Beim ersten Lauf fielen zwei Tests, beide waren MEINE Fehler: cluster rundet auf 3 Stellen (Invariante zu streng) und zwei inhaltsgleiche dicts sind == (Reihenfolge muss ueber die Identitaet geprueft werden). Genau dafuer sind Property-Tests da. Mutationsprobe: Rundung verbogen -> 2 Tests fallen. BEGRUENDET ABGELEHNT: numba (Backtests 11 s/12k Bars = nicht der Engpass, und ein Rewrite verletzt die Bit-Gleichheits-Regel) · statsmodels/arch (jetzt 15 Zeilen stdlib mit eigener Kalibrierung) · polars/pyarrow (kein DataFrame- Workload) · loguru/structlog/rich (das Logformat ist forensisch tragend) · tenacity (Wiederholungen haben eigene Semantik, _mark_dead) · httpx (requests laeuft) · pandas_ta (Indikatoren sind validiert, ein Austausch verschoebe jede dokumentierte Zahl) · mypy (kaum Annotationen, ty deckt die Aritaet ab). requirements.txt (Laufzeit) bleibt UNVERAENDERT - auf einer frischen Maschine soll der Bot mit moeglichst kleiner Oberflaeche wieder laufen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
85 lines
4.0 KiB
Python
85 lines
4.0 KiB
Python
"""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
|