Bibliotheken geprueft: zwei Ertraege, keine neue Laufzeit-Abhaengigkeit

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>
This commit is contained in:
Axel Hocks
2026-08-20 19:57:31 +02:00
co-authored by Claude Opus 5
parent 0386497a79
commit 3391e04f32
5 changed files with 295 additions and 0 deletions
+75
View File
@@ -169,3 +169,78 @@ def zeig_paarweise(name: str, basis: dict, variante: dict) -> bool:
print(f" {name:<28} Δ je Fall {m:>+.4f} KI [{lo:+.4f}{hi:+.4f}] "
f"n={n} {'✅ ohne 0' if ok else '⚠ enthaelt 0'}")
return ok
def autokorrelation(x, lag: int = 1) -> float:
"""Autokorrelation zum gegebenen Lag. 0 = unabhaengig, ->1 = stark geclustert."""
n = len(x)
if n <= lag + 1:
return 0.0
m = sum(x) / n
nen = sum((v - m) ** 2 for v in x)
if nen <= 0:
return 0.0
zae = sum((x[i] - m) * (x[i + lag] - m) for i in range(n - lag))
return max(-1.0, min(1.0, zae / nen))
def block_ci95(x, blocklen: int | None = None, wiederholungen: int = 2000,
seed: int = 7):
"""95-%-Intervall fuer den Mittelwert bei ABHAENGIGEN Beobachtungen.
⚠⚠ WOZU — die Luecke stand seit dem 2026-08-07 woertlich in
`backtest_cost_gate.ci95`: *"Fuer ueberlappende Beobachtungen ist er ZU
OPTIMISTISCH ... dafuer braeuchte es einen Block-Bootstrap — `ci95` ist
dafuer NICHT gedacht."* Der Behelf war bis jetzt `entkoppeln()`, und der
kostet teuer: auf `pbreak_predictions` bleiben von **4.702 Zeilen nur 552**
(88 % Datenverlust) — und er VERSCHIEBT die Schaetzung, weil er
systematisch die ERSTE Beruehrung je Level+Stunde nimmt (0,399 gegen 0,427).
Der Moving-Block-Bootstrap zieht stattdessen zusammenhaengende BLOECKE und
behaelt damit die Abhaengigkeit INNERHALB eines Blocks. Auf denselben Daten:
naiv n=4702 Breite 0,028 <- 3x zu eng
entkoppelt n= 552 Breite 0,083 <- 88 % weg, Schaetzung verschoben
Block L=30 n=4702 Breite 0,081 <- gleiche Breite, ALLE Daten
✅ KALIBRIERUNG GEMESSEN, nicht behauptet (AR(1)-Reihen, wahres Mittel
bekannt, 200 Laeufe je Zelle — Ueberdeckung soll 95 % sein):
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 %
⚠ EHRLICH ZU DEN GRENZEN: (a) bei rho=0 kostet es ~3 Punkte Ueberdeckung
gegenueber dem naiven Verfahren — bei UNABHAENGIGEN Daten bleibt `ci95` die
bessere Wahl; (b) bei sehr starker Abhaengigkeit (rho=0,97) deckt auch der
Block-Bootstrap nur 83 %, ist also weiter zu optimistisch, nur eben viel
weniger. Er ist eine Verbesserung, keine Garantie.
`blocklen=None` waehlt automatisch aus der gemessenen Lag-1-Autokorrelation
(mehr Abhaengigkeit -> laengere Bloecke). Rueckgabe (lo, hi, blocklen) bzw.
(None, None, 0) bei zu kleiner Stichprobe.
"""
import random as _r
x = [float(v) for v in x]
n = len(x)
if n < 40:
return None, None, 0
if blocklen is None:
rho = abs(autokorrelation(x))
# rho 0 -> ~n^(1/3); stark abhaengig -> deutlich laenger. Gedeckelt auf
# n//5, damit noch mindestens 5 Bloecke gezogen werden.
basis = max(2, int(round(n ** (1 / 3))))
blocklen = max(2, min(n // 5, int(round(basis * (1.0 + 6.0 * rho)))))
if n < 2 * blocklen:
return None, None, 0
r = _r.Random(seed)
k = -(-n // blocklen)
mittel = []
for _ in range(wiederholungen):
s = []
for _ in range(k):
i = r.randrange(n - blocklen + 1)
s.extend(x[i:i + blocklen])
mittel.append(sum(s[:n]) / n)
mittel.sort()
return (mittel[int(0.025 * wiederholungen)],
mittel[int(0.975 * wiederholungen)], blocklen)