Live-Backtest-Vergleich in die Pipeline: core/stichprobe.py + Abschnitt E
Konsequenz aus dem Messfehler vom 07.08.: der Vergleich lief nur, wenn jemand
daran dachte - und als er lief, war er falsch gebaut.
1) core/stichprobe.py - die Praevention ist KONSTRUKTIV, nicht ermahnend:
entkoppeln() / anteil_ci() / unterscheidbar() / schluessel_level_stunde().
Regel: vor jeder berichteten Rate entkoppeln, jede Aussage ueber einen
Unterschied mit Konfidenzintervall belegen. Die Zeilen in
pbreak_predictions und recommendations sind nicht unabhaengig.
8 Tests, darunter einer, der die reale Konstellation nachspielt
(roh 77 %, entkoppelt 33 %).
Fallstrick beim Bau, gefunden weil ich es laufen liess: ein sqlite3-Cursor
liefert Tupel, keine dicts - die Feldangabe akzeptiert jetzt Namen UND
Spaltennummern.
2) analyze_divergence.sec_d0 war selbst der Fehler. Es mittelte AVG(p_break)
ueber ALLE Rohzeilen und flaggte bei Delta > 8 Pp:
vorher 1058 rohe Zeilen, Delta +8,7 Pp -> ALARM
jetzt 110 entkoppelte, 37,6 % liegt in [37,3 ... 55,5] % -> OK
Der Waechter haette heute also einen Fehlalarm produziert - genau den, dem
ich elf Hypothesen lang nachgegangen bin.
3) NEU: Abschnitt E "GLEICHES FENSTER" (--backtest). Rechnet den Backtest ueber
genau den Live-Zeitraum und haelt ihn gegen das Intervall der entkoppelten
Live-Rate. Erster Lauf: live 46,4 % [37,3 ... 55,5] gegen Backtest 43,9 %
-> im Intervall, kein Befund.
4) Eingebaut in den Wochenreport (weekly_review._divergenz_section, montags
07:30 per Mail + Telegram), mit demselben Fail-safe wie der
Squeeze-Abschnitt. Bewusst NICHT in den pre-commit-Hook: der braucht MT5 und
Minuten, Stufe 1 muss bei ~5 s bleiben.
Ausserdem ein Rechenfehler in meinem ersten Entwurf behoben (p_break steht in
der DB bereits in Prozent, ich hatte zusaetzlich skaliert -> "3763,9 %") und
ein \n-Heredoc-Fall, der genau gegen die dokumentierte Regel verstiess.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1f58049049
commit
3f1f19087d
@@ -0,0 +1,83 @@
|
||||
"""Tests für `core/stichprobe.py` — die Stichproben-Hygiene, ohne die am
|
||||
2026-08-07 DREIMAL derselbe Fehler passiert ist (Fälligkeitsbedingung,
|
||||
Divergenz-Wächter, und meine eigene Auswertung).
|
||||
|
||||
⚠ Wichtigster Test unten: `test_faengt_den_fehler_vom_07_08` spielt die reale
|
||||
Konstellation nach — eine geclusterte Stichprobe, deren rohe Rate weit von der
|
||||
entkoppelten abweicht. Ohne ihn wäre nicht belegt, dass das Modul den Fall
|
||||
überhaupt fängt.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from core.stichprobe import (anteil_ci, entkoppeln, schluessel_level_stunde,
|
||||
unterscheidbar)
|
||||
|
||||
|
||||
def test_entkoppeln_behaelt_erste_je_gruppe():
|
||||
zeilen = [(3600, 75.0), (3601, 75.0), (3602, 75.004), (7200, 75.0), (7201, 80.0)]
|
||||
ent = entkoppeln(zeilen, schluessel_level_stunde(1, 0))
|
||||
# 75,00 in Stunde 1 (auch 75,004 rundet dorthin), 75,00 in Stunde 2, 80,00
|
||||
assert len(ent) == 3
|
||||
assert ent[0] == (3600, 75.0) # die ERSTE bleibt, nicht die letzte
|
||||
assert [z[0] for z in ent] == [3600, 7200, 7201] # Reihenfolge erhalten
|
||||
|
||||
|
||||
def test_entkoppeln_leer_und_ohne_duplikate():
|
||||
assert entkoppeln([], schluessel_level_stunde(1, 0)) == []
|
||||
z = [(0, 1.0), (3600, 2.0), (7200, 3.0)]
|
||||
assert entkoppeln(z, schluessel_level_stunde(1, 0)) == z
|
||||
|
||||
|
||||
def test_feldzugriff_tupel_dict_und_row():
|
||||
"""⚠ Der erste Einbau scheiterte an genau diesem Punkt: ein sqlite3-Cursor
|
||||
liefert ohne `row_factory` TUPEL, nicht dicts."""
|
||||
k_idx = schluessel_level_stunde(1, 0)
|
||||
k_name = schluessel_level_stunde("level", "ts")
|
||||
assert k_idx((3600, 75.0)) == (75.0, 1)
|
||||
assert k_name({"ts": 3600, "level": 75.0}) == (75.0, 1)
|
||||
|
||||
|
||||
def test_anteil_ci_grundfaelle():
|
||||
a, lo, hi = anteil_ci([1] * 50 + [0] * 50, 100)
|
||||
assert a == pytest.approx(0.5)
|
||||
assert lo < 0.5 < hi
|
||||
# zu wenige Punkte -> KEIN Intervall vortaeuschen
|
||||
a, lo, hi = anteil_ci([1, 0, 1], 3)
|
||||
assert lo is None and hi is None
|
||||
# Zaehl-Variante muss dasselbe liefern wie die Listen-Variante
|
||||
assert anteil_ci(50, 100)[0] == pytest.approx(anteil_ci([1] * 50 + [0] * 50, 100)[0])
|
||||
|
||||
|
||||
def test_anteil_ci_reproduzierbar():
|
||||
"""Fester `seed` — sonst schwankt eine Alarmschwelle von Lauf zu Lauf."""
|
||||
assert anteil_ci([1] * 30 + [0] * 70, 100) == anteil_ci([1] * 30 + [0] * 70, 100)
|
||||
|
||||
|
||||
def test_unterscheidbar():
|
||||
y = [1] * 45 + [0] * 55 # 45 %
|
||||
assert unterscheidbar(y, 100, 0.44) is False # nah dran -> nicht belegbar
|
||||
assert unterscheidbar(y, 100, 0.05) is True # weit weg -> belegbar
|
||||
assert unterscheidbar([1, 0], 2, 0.5) is None # zu wenig -> keine Aussage
|
||||
|
||||
|
||||
def test_faengt_den_fehler_vom_07_08():
|
||||
"""Die reale Konstellation: 10 Level, je einmal 'break' und danach viele
|
||||
Wiederholungen derselben Lage. Roh sieht die Rate ganz anders aus als
|
||||
entkoppelt — genau das hat am 07.08. eine Untersuchung mit elf widerlegten
|
||||
Hypothesen ausgeloest."""
|
||||
zeilen = []
|
||||
for i in range(10):
|
||||
ts = 3600 * i
|
||||
zeilen.append((ts, 75.0 + i, 1)) # erster Touch: Durchbruch
|
||||
zeilen.extend((ts + 60 + j, 75.0 + i, 1) for j in range(19)) # 19 Wiederholungen
|
||||
for i in range(10, 30):
|
||||
zeilen.append((3600 * i, 75.0 + i, 0)) # 20 einzelne Abpraller
|
||||
roh = sum(z[2] for z in zeilen) / len(zeilen)
|
||||
ent = entkoppeln(zeilen, schluessel_level_stunde(1, 0))
|
||||
entr = sum(z[2] for z in ent) / len(ent)
|
||||
assert roh > 0.75, "Aufbau falsch — roh muesste stark ueberhoeht sein"
|
||||
assert entr == pytest.approx(1 / 3, abs=0.02)
|
||||
# Die Verzerrung muss GROSS sein, sonst prueft der Test nichts
|
||||
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
|
||||
Reference in New Issue
Block a user