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
+120
-15
@@ -24,14 +24,20 @@ GEPRÜFT WIRD:
|
||||
„feste TF" — je mehr Wechsel, desto weiter weg)
|
||||
D) Merkmals-Drift der eingebetteten Modelle: Live-Mittel gegen `_PB_MU`/`_PB_SD`
|
||||
|
||||
Aufruf: python analyze_divergence.py [tage] (Default 7)
|
||||
E) GLEICHES FENSTER — Backtest auf genau dem Live-Zeitraum, gegen das
|
||||
Konfidenzintervall der ENTKOPPELTEN Live-Rate (nur mit --backtest)
|
||||
|
||||
Aufruf: python analyze_divergence.py [tage] [--backtest] (Default 7)
|
||||
"""
|
||||
import re
|
||||
import sqlite3
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from core.stichprobe import anteil_ci, entkoppeln, schluessel_level_stunde
|
||||
|
||||
DB = "oil_widget_history.db"
|
||||
_BROKER_OFF = 10800 # Brokerzeit = UTC+3 (candles/copy_rates sind Broker-Wallclock)
|
||||
LOG = "oil_widget.log"
|
||||
|
||||
# Erwartungswerte aus den Backtests — die Messlatte
|
||||
@@ -190,21 +196,113 @@ def sec_d0(c, t0):
|
||||
t0, cut = _clamp(t0, "pbreak_model")
|
||||
if cut:
|
||||
print(f" (nur Vorhersagen seit dem Nachtraining am {_EPOCHS['pbreak_model']})")
|
||||
c.execute("""SELECT AVG(p_break), AVG(CASE WHEN outcome='break' THEN 100.0 ELSE 0 END),
|
||||
COUNT(*) FROM pbreak_predictions
|
||||
WHERE ts>=? AND outcome IS NOT NULL""", (t0,))
|
||||
pm, rm, n = c.fetchone()
|
||||
if not n or n < 30:
|
||||
print(f" nur {n or 0} ausgewertete Vorhersagen seit dem Nachtraining — zu wenig.")
|
||||
# ⚠⚠ ENTKOPPELN, BEVOR IRGENDEINE RATE BERICHTET WIRD (Fix 2026-08-07).
|
||||
# Vorher stand hier ein schlichtes `AVG(...)` über ALLE Rohzeilen. Die Zeilen
|
||||
# sind aber NICHT unabhängig — dieselbe Position pendelt vielfach um dasselbe
|
||||
# Level. Real am 07.08.: roh 922 Zeilen mit 52,6 % Bruchrate, entkoppelt 93
|
||||
# Zeilen mit 45,2 %. Der Wächter hätte auf der Rohzahl Alarm geschlagen
|
||||
# (Δ +10,8 Pp > 8), obwohl der Backtest DESSELBEN Zeitfensters bei 43,4 % lag
|
||||
# und damit im 95-%-Intervall der entkoppelten Rate [35,5 … 54,8] liegt.
|
||||
# Genau diese Verwechslung hat an dem Tag eine Untersuchung mit elf
|
||||
# widerlegten Hypothesen ausgelöst.
|
||||
rows = c.execute("""SELECT ts, level, p_break, outcome FROM pbreak_predictions
|
||||
WHERE ts>=? AND outcome IS NOT NULL ORDER BY ts""",
|
||||
(t0,)).fetchall()
|
||||
roh = len(rows)
|
||||
ent = entkoppeln(rows, schluessel_level_stunde(1, 0)) # Tupel: ts=0, level=1
|
||||
n = len(ent)
|
||||
if n < 30:
|
||||
print(f" nur {n} ENTKOPPELTE Vorhersagen seit dem Nachtraining "
|
||||
f"(roh {roh}) — zu wenig.")
|
||||
print(f" (Das ist KEIN Entwarnungssignal, sondern fehlende Evidenz. "
|
||||
f"Fälligkeit: Messung `pbreak_accuracy_v2`, ≥800 Vorhersagen.)")
|
||||
f"Fälligkeit: Messung `pbreak_accuracy_v2`, ≥350 ENTKOPPELTE.)")
|
||||
return
|
||||
gap = (rm or 0) - (pm or 0)
|
||||
print(f" n={n} Ø-Vorhersage {pm:.1f} % echte Rate {rm:.1f} % "
|
||||
f"Δ {gap:+.1f} Pp {_flag(abs(gap) <= 8)}")
|
||||
if abs(gap) > 8:
|
||||
print(" ⚠ Das Modell ist systematisch verschoben — es bewertet live eine")
|
||||
print(" andere Situation als beim Training (Deployment-Drift).")
|
||||
# ⚠ `p_break` steht in der DB bereits in PROZENT (0–100) — nicht nochmal
|
||||
# skalieren. Der erste Entwurf tat es und meldete „Ø-Vorhersage 3763,9 %".
|
||||
pm = sum(float(r[2] or 0) for r in ent) / n
|
||||
treffer = [1 if r[3] == "break" else 0 for r in ent]
|
||||
rate, lo, hi = anteil_ci(treffer, n)
|
||||
rm = 100 * rate
|
||||
gap = rm - pm
|
||||
# ⚠ Geflaggt wird nur, wenn die Ø-Vorhersage AUSSERHALB des Intervalls der
|
||||
# echten Rate liegt — ein Punktabstand allein ist auf dieser Datenlage keine
|
||||
# Aussage. Die alte 8-Pp-Schwelle ohne Intervall war genau der Fehler.
|
||||
drin = lo is not None and lo <= pm / 100 <= hi
|
||||
print(f" n={n} entkoppelt (roh {roh}) Ø-Vorhersage {pm:.1f} % "
|
||||
f"echte Rate {rm:.1f} % Δ {gap:+.1f} Pp")
|
||||
if lo is not None:
|
||||
print(f" 95-%-KI der echten Rate: [{100*lo:.1f} … {100*hi:.1f}] % "
|
||||
f"{_flag(bool(drin))}")
|
||||
if not drin:
|
||||
print(" ⚠ Die Ø-Vorhersage liegt AUSSERHALB des Intervalls — das Modell")
|
||||
print(" bewertet live eine andere Situation als beim Training")
|
||||
print(" (Deployment-Drift). Vor einer Reparatur: den Backtest auf")
|
||||
print(" DASSELBE Zeitfenster einschränken (s. Abschnitt E).")
|
||||
|
||||
|
||||
def sec_e(c, t0):
|
||||
"""Der Vergleich, der heute gefehlt hat: der Backtest auf DEMSELBEN Fenster.
|
||||
|
||||
⚠⚠ WARUM (2026-08-07). Eine Live-Rate gegen einen Backtest-Durchschnitt aus
|
||||
208 Tagen zu halten, ist kein Vergleich — es mischt den Regime-Unterschied in
|
||||
die Aussage. Real gemessen: die Live-Bruchrate lag bei 52,7 %, der
|
||||
Langfrist-Backtest bei 38,6 %. Auf DIESELBEN sechs Tage eingeschränkt liefert
|
||||
der Backtest **43,4 %**, und entkoppelt liegt live bei **45,2 %** — kein
|
||||
belegbarer Unterschied. Von 14,1 Pp „Lücke" waren **34 % Zeitraum, 53 %
|
||||
Clusterung, der Rest Rauschen**.
|
||||
|
||||
⚠ Braucht MT5 und ~1–2 min, deshalb nur mit `--backtest`.
|
||||
"""
|
||||
print("\n" + "=" * 84)
|
||||
print(" E) GLEICHES FENSTER — Backtest auf genau dem Live-Zeitraum")
|
||||
print("=" * 84)
|
||||
rows = c.execute("""SELECT ts, level, outcome FROM pbreak_predictions
|
||||
WHERE ts>=? AND outcome IS NOT NULL ORDER BY ts""",
|
||||
(t0,)).fetchall()
|
||||
ent = entkoppeln(rows, schluessel_level_stunde(1, 0)) # Tupel: ts=0, level=1
|
||||
if len(ent) < 30:
|
||||
print(f" nur {len(ent)} entkoppelte Live-Vorhersagen — zu wenig.")
|
||||
return
|
||||
rate, lo, hi = anteil_ci([1 if r[2] == "break" else 0 for r in ent], len(ent))
|
||||
t_von, t_bis = rows[0][0], rows[-1][0]
|
||||
try:
|
||||
import MetaTrader5 as mt5
|
||||
import numpy as np
|
||||
|
||||
from backtest_pbreak_retrain import (_atr_series, _ema_series,
|
||||
build_levels, collect, _N_BARS)
|
||||
from core.wave_rec import _EMA_FAST, _EMA_SLOW
|
||||
mt5.initialize()
|
||||
sym = next((s for s in ("SpotCrude", "USOIL", "WTI", "XTIUSD")
|
||||
if mt5.symbol_info(s)), None)
|
||||
bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, 20000)
|
||||
mt5.shutdown()
|
||||
T = [int(b["time"]) - _BROKER_OFF for b in bars]
|
||||
H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars]
|
||||
C = [float(b["close"]) for b in bars]
|
||||
import bisect
|
||||
a = max(bisect.bisect_left(T, t_von), _N_BARS)
|
||||
b = bisect.bisect_right(T, t_bis)
|
||||
if b - a < 200:
|
||||
print(" M5-Daten decken das Live-Fenster nicht ab."); return
|
||||
EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW)
|
||||
AT = _atr_series(H, L, C)
|
||||
s = collect(a, b, H, L, C, EF, ES, AT, build_levels(H, L))
|
||||
bt = float(np.mean([e["y"] for e in s])) if s else float("nan")
|
||||
except Exception as e: # pragma: no cover
|
||||
print(f" Backtest nicht möglich ({e})"); return
|
||||
|
||||
print(f" Fenster: {datetime.fromtimestamp(t_von):%d.%m.} bis "
|
||||
f"{datetime.fromtimestamp(t_bis):%d.%m.}")
|
||||
print(f" LIVE entkoppelt n={len(ent):<5} Bruchrate {100*rate:.1f} %"
|
||||
+ (f" 95-%-KI [{100*lo:.1f} … {100*hi:.1f}] %" if lo is not None else ""))
|
||||
print(f" BACKTEST gleiches Fenster n={len(s):<5} Bruchrate {100*bt:.1f} %")
|
||||
if lo is not None:
|
||||
drin = lo <= bt <= hi
|
||||
print(f" {_flag(bool(drin))} "
|
||||
+ ("Backtest liegt im Intervall — kein belegbarer Unterschied."
|
||||
if drin else
|
||||
"Backtest liegt AUSSERHALB — echte Divergenz, nachgehen."))
|
||||
|
||||
|
||||
def sec_d(c, t0):
|
||||
@@ -255,7 +353,9 @@ def sec_d(c, t0):
|
||||
|
||||
|
||||
def main():
|
||||
days = int(sys.argv[1]) if len(sys.argv) > 1 else 7
|
||||
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
||||
mit_bt = "--backtest" in sys.argv
|
||||
days = int(args[0]) if args else 7
|
||||
t0 = (datetime.now() - timedelta(days=days)).timestamp()
|
||||
db = sqlite3.connect(DB); c = db.cursor()
|
||||
print("=" * 84)
|
||||
@@ -266,6 +366,11 @@ def main():
|
||||
sec_b(c, t0)
|
||||
sec_c(t0)
|
||||
sec_d0(c, t0)
|
||||
if mit_bt:
|
||||
sec_e(c, _clamp(t0, "pbreak_model")[0])
|
||||
else:
|
||||
print()
|
||||
print(" (Abschnitt E — Backtest auf gleichem Fenster — mit `--backtest`)")
|
||||
sec_d(c, t0)
|
||||
db.close()
|
||||
print("\n" + "=" * 84)
|
||||
|
||||
Reference in New Issue
Block a user