From 3391e04f325787252f63759c1162a617f90c4e47 Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Thu, 20 Aug 2026 19:57:31 +0200 Subject: [PATCH] Bibliotheken geprueft: zwei Ertraege, keine neue Laufzeit-Abhaengigkeit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CLAUDE.md | 73 ++++++++++++++++++++++++++++++++++ core/stichprobe.py | 75 ++++++++++++++++++++++++++++++++++ requirements-dev.txt | 8 ++++ tests/test_invarianten.py | 84 +++++++++++++++++++++++++++++++++++++++ tests/test_stichprobe.py | 55 +++++++++++++++++++++++++ 5 files changed, 295 insertions(+) create mode 100644 tests/test_invarianten.py diff --git a/CLAUDE.md b/CLAUDE.md index ff4965c..3942b24 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9184,6 +9184,79 @@ geändert — Sizing/Alarm sind User-Entscheidungen): nichts: die Vormerkung hängt an `_close_notify_min > 0`. **Der Bot meldet derzeit gar keinen Trade-Abschluss.** +## ⚠⚠ WEITERE PYTHON-BIBLIOTHEKEN? — ZWEI ERTRÄGE, KEINE NEUE LAUFZEIT-ABHÄNGIGKEIT (2026-08-20) + +User-Frage. **Nicht nach Katalog beantwortet, sondern gegen die dokumentierten +Schwachstellen des Projekts** — und die beiden Kandidaten, die durchkamen, +wurden vorher **gemessen**, nicht empfohlen. + +✅✅ **ERTRAG 1 — BLOCK-BOOTSTRAP (`core/stichprobe.block_ci95`), und er braucht +GAR KEINE Bibliothek.** Die Lücke stand seit dem 07.08. wörtlich in +`backtest_cost_gate.ci95`: *„für überlappende Beobachtungen ist er ZU +OPTIMISTISCH … dafür bräuchte es einen Block-Bootstrap — `ci95` ist dafür NICHT +gedacht."* Behelf war bisher `entkoppeln()`. **Auf echten Projektdaten +(`pbreak_predictions`, Bruchrate) kostet der Behelf dramatisch:** +| Verfahren | n | 95-%-KI | Breite | +|---|---|---|---| +| naiv, alle Zeilen | 4.702 | [0,413 … 0,442] | **0,028** ⚠ 3× zu eng | +| **entkoppelt (bisheriger Weg)** | **552** | [0,357 … 0,440] | 0,083 | +| **Block-Bootstrap L=30** | **4.702** | [0,387 … 0,469] | **0,081** | +**Der Block-Bootstrap trifft die Breite des entkoppelten Verfahrens — behält +aber alle 4.702 Beobachtungen statt 552 (88 % Datenverlust).** +⚠⚠ **Und das Entkoppeln kostet nicht nur Genauigkeit, es VERSCHIEBT die +Schätzung**: 0,399 gegen 0,427. Es nimmt systematisch die ERSTE Berührung je +Level+Stunde — eine verzerrte Teilstichprobe, kein neutraler Ausschnitt. + +✅ **KALIBRIERUNG GEMESSEN statt behauptet** (AR(1)-Reihen mit bekanntem Mittel, +200 Läufe je Zelle, Soll 95 %): +| ρ | 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 eigentliche Befund: bei ρ=0,97 behauptet sie 95 % +Sicherheit und liegt in **18 %** der Fälle richtig. +⚠ **Ehrlich zu den Grenzen:** (a) bei UNABHÄNGIGEN Daten kostet der Block ~3 +Punkte Überdeckung — dort bleibt `ci95` die bessere Wahl; (b) bei ρ=0,97 deckt +auch er nur 83 %. **Eine Verbesserung, keine Garantie** — genau so steht es im +Docstring. +⚠ Blocklänge wird aus der gemessenen Lag-1-Autokorrelation gewählt (auf den +Live-Daten: ρ=0,762 → L=95). 4 Tests, darunter die Überdeckungs-Probe. + +✅✅ **ERTRAG 2 — `hypothesis` (dev-only), aufgenommen NACH einer Gegenprobe.** +Die Suite prüft bisher **Beispiele**, und die wähle ich selbst aus — genau dort +sitzt der blinde Fleck. Die teuersten Fehler des Projekts saßen auf **Grenzen**: +`room_info` (10.08.) musste `frei` auf dem UNGERUNDETEN Abstand entscheiden, +weil 0,5951 gerundet durchs 0,6-Gate rutscht. +**Gegenprobe: hypothesis findet genau diesen Fall — `dist=0.59765625` — in +Sekunden.** Erst danach aufgenommen. +⚠ **Neu `tests/test_invarianten.py`** (4 Eigenschafts-Tests auf REINEN +Funktionen: `sr_levels.cluster`, `stichprobe.entkoppeln`, `autokorrelation`). +Per `importorskip` — **die Suite läuft auch ohne die Bibliothek**. +⚠⚠ **Beim ersten Lauf fielen zwei Tests — beide waren MEINE Fehler, nicht die +des Codes**, und das ist genau der Nutzen: (1) `cluster` rundet auf 3 Stellen +(dokumentiert), das Ergebnis darf also 0,0005 ausserhalb der Eingabespanne +liegen — meine Invariante war zu streng; (2) zwei inhaltsgleiche dicts sind +`==`, meine Reihenfolge-Prüfung musste über die **Identität** laufen. +✅ Mutationsprobe: Rundung in `sr_levels` verbogen → **2 Tests fallen**. + +⚠⚠ **WAS BEGRÜNDET ABGELEHNT WURDE — jeweils mit Messung oder Regelbezug:** +| Kandidat | Urteil | +|---|---| +| **numba** (installiert, ungenutzt) | Backtests laufen 11 s / 12k Bars — **nicht der Engpass**. Und ein Loop-Rewrite verletzt die Migrations-Regel (Bit-Gleichheit). | +| **statsmodels / arch** | hätten HAC bzw. Block-Bootstrap — jetzt in **15 Zeilen stdlib** erledigt, mit eigener Kalibrierung. Kleinere Oberfläche schlägt fremde Abhängigkeit. | +| **polars / pyarrow** | es gibt keinen DataFrame-Workload; die Backtests laufen auf Listen. | +| **loguru / structlog / rich** | das Logformat ist **forensisch tragend** (18-h-Copilot-Ausfall, stille Fehlbuchung, die 269 Pending-Zeilen). Es umzustellen kostet Beweismaterial. | +| **tenacity** | Wiederholungen haben hier eigene Semantik (`agent._mark_dead`: hart 401/402/403/404 → 30 min, Timeout/429 bewusst NICHT). | +| **httpx** (schon da als openai-Abhängigkeit) | `requests` läuft; ein Wechsel ohne Anlass ist Bewegung ohne Ertrag. | +| **pandas_ta** (installiert!) | alle Indikatoren sind handgeschrieben und **validiert**; ein Austausch würde jede dokumentierte Zahl verschieben. | +| **mypy** | kaum Annotationen vorhanden; `ty` deckt seit 19.08. die Aritäts-Fälle ab. | + +✅ **`requirements.txt` (Laufzeit) bleibt unverändert** — das war die Vorgabe an +mich selbst: auf einer frischen Maschine soll der Bot mit möglichst kleiner +Oberfläche wieder laufen. Ergänzt wurde ausschliesslich `requirements-dev.txt`. + ## ⚠⚠⚠ P(break) IST LIVE **INVERTIERT** — und es steuert echtes Geld (2026-08-19) Die fällige Messung ist entscheidbar geworden: **n=530 entkoppelt** gegen die diff --git a/core/stichprobe.py b/core/stichprobe.py index a7a17b1..3230989 100644 --- a/core/stichprobe.py +++ b/core/stichprobe.py @@ -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) diff --git a/requirements-dev.txt b/requirements-dev.txt index 234854d..64f2ee9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,6 +13,14 @@ pytest==9.1.1 ruff==0.16.1 # ersetzt pyflakes: gleiche Befunde, deutlich schneller pyflakes==3.4.0 # Rueckfall in `tools/check_nfalle.py`, falls ruff fehlt +# Eigenschafts-Tests auf den REINEN Funktionen (tests/test_invarianten.py). +# ⚠ Aufgenommen NACH einer Gegenprobe, nicht auf Verdacht: hypothesis findet den +# real am 10.08. gefundenen Rundungsfehler (`frei` auf gerundetem Abstand -> +# 0,59765625 rutscht durchs 0,6-Gate) in Sekunden. Die Suite prueft sonst nur +# BEISPIELE, und die waehle ich selbst aus - genau dort sitzt der blinde Fleck. +# ⚠ Optional: `tests/test_invarianten.py` ueberspringt sich per `importorskip`, +# die Suite laeuft also auch ohne. +hypothesis==6.165.10 # ── Messung / Backtests ────────────────────────────────────────────────── numpy==2.2.6 diff --git a/tests/test_invarianten.py b/tests/test_invarianten.py new file mode 100644 index 0000000..99f430c --- /dev/null +++ b/tests/test_invarianten.py @@ -0,0 +1,84 @@ +"""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 diff --git a/tests/test_stichprobe.py b/tests/test_stichprobe.py index 27522ad..f34386d 100644 --- a/tests/test_stichprobe.py +++ b/tests/test_stichprobe.py @@ -81,3 +81,58 @@ def test_faengt_den_fehler_vom_07_08(): assert roh - entr > 0.3 # ...und die rohe Rate muss ausserhalb des Intervalls der entkoppelten liegen assert unterscheidbar([z[2] for z in ent], len(ent), roh) is True + + +# ── Block-Bootstrap fuer abhaengige Beobachtungen (2026-08-20) ────────────── +import random as _rnd + +from core.stichprobe import autokorrelation, block_ci95 + + +def _ar1(n, rho, seed): + """AR(1)-Reihe mit Mittelwert 0 - so sehen ueberlappende Beobachtungen aus.""" + r = _rnd.Random(seed) + out, v = [], 0.0 + for _ in range(n): + v = rho * v + r.gauss(0, 1) * (1 - rho ** 2) ** 0.5 + out.append(v) + return out + + +def test_autokorrelation_erkennt_unabhaengig_und_geclustert(): + assert abs(autokorrelation(_ar1(3000, 0.0, 1))) < 0.10 + assert autokorrelation(_ar1(3000, 0.9, 2)) > 0.7 + + +def test_block_ci_ist_BREITER_als_das_naive_bei_abhaengigen_daten(): + """Der ganze Zweck: bei Clusterung muss das Intervall breiter werden. + + ⚠ Genau das war die dokumentierte Luecke - `ci95` unterstellt unabhaengige + Ziehungen und ist bei ueberlappenden Beobachtungen zu optimistisch. + """ + x = _ar1(2000, 0.9, 3) + lo_b, hi_b, bl = block_ci95(x) + assert bl > 2 + naiv = 2 * 1.96 * (sum((v - sum(x) / len(x)) ** 2 for v in x) + / len(x)) ** 0.5 / len(x) ** 0.5 + assert (hi_b - lo_b) > 1.5 * naiv, ( + "Block-KI ist nicht breiter als das naive - die Abhaengigkeit wird " + "nicht erfasst") + + +def test_block_ci_deckt_den_wahren_mittelwert_meistens(): + """Kalibrierung: bei AR(1) mit Mittel 0 muss die 0 meistens drin liegen. + + ⚠ Bewusst 'meistens' und nicht 'immer': gemessen deckt der Block-Bootstrap + bei rho=0,9 rund 90 % (das naive Verfahren nur 40 %). Er ist eine + Verbesserung, keine Garantie - und der Test haelt genau das fest. + """ + treffer = sum(1 for s in range(40) + if (lambda r: r[0] <= 0 <= r[1])(block_ci95(_ar1(800, 0.9, s), + wiederholungen=400))) + assert treffer >= 30, f"nur {treffer}/40 Ueberdeckung" + + +def test_block_ci_lehnt_zu_kleine_stichproben_ab(): + lo, hi, bl = block_ci95([1.0, 2.0, 3.0]) + assert lo is None and hi is None and bl == 0