Files
AH-Oil-Trader/backtest_pbreak_regime.py
T
Axel HocksandClaude Opus 5 2e89724368 P(break) Niveau-Reparatur gemessen: beide Varianten fallen durch
Folgeauftrag zur B4-Pruefung. Hypothese war: das Modell rankt brauchbar, nur
sein Niveau haengt an einer wandernden Basisrate. Geprueft wurden ein
Regime-Merkmal (ATR/gleitender Median) und ein laufend nachgefuehrter
Achsenabschnitt, Regel vorab fixiert.

collect() in backtest_pbreak_retrain.py hat dafuer einen ADDITIVEN
REG-Parameter bekommen (Default None), Ausgabe ohne ihn bitgenau geprueft
(4371 Zeilen, 0 Abweichungen).

Auf Backtest-Daten besteht die Roll-Achse (Kalib 2,61->1,95 und 10,51->3,25 Pp,
AUC leicht besser), das Regime-Merkmal faellt durch (H2->H1 schlechter).

Auf LIVE-Daten (n=921 ab 01.08.) faellt auch die Roll-Achse durch: Kalib
14,16 -> 10,64 Pp (also nicht repariert), AUC 0,531 -> 0,516 (ueber der
Toleranz), und der Nachbar n=800 kippt auf 15,14 Pp = schlechter als der
Ausgangswert.

Grund: die oberste Klasse bleibt INVERTIERT (69 % vorhergesagt, 17,2 % real).
Eine Niveau-Verschiebung kann eine invertierte Klasse nicht reparieren. Die
Ausgangshypothese ist damit widerlegt.

Drei Erklaerungen fuer die Basisraten-Luecke geprueft, alle widerlegt:
  Sammelart (live 1x je Beruehrung vs Backtest jeder Bar): -1,7 Pp, falsche
    Richtung
  Vola-Regime: Bruchrate ueber 5 Quintile flach (37,0 bis 39,2 %)
  aktuelles Regime ungewoehnlich: letzte 7 Tage Median 0,96 = normal
Der flache Vola-Befund erklaert zugleich, warum das Regime-Merkmal nichts
bringt.

Kontrolle: das Modell VOR dem 31.07. hat auf diesen Daten AUC 0,364/0,406 -
unter 0,5, also invertiert. Reproduziert exakt den Grund fuer das
Nachtrainieren.

Nichts gebaut. Naechster Schritt ist kein drittes Fitten, sondern die einzige
ungeprueft gebliebene Divergenz: die Level-Auswahl selbst (Backtest baut sie
mit pick_level/_cluster nach, live entscheidet _draw_levels).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 11:18:35 +02:00

209 lines
9.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""P(break): liegt der Live-Fehler am NIVEAU (Basisrate) oder an der RANGFOLGE?
ANLASS (2026-08-07). Die B4-Prüfung des am 31.07. nachtrainierten Modells zeigt
zwei getrennte Dinge:
· KALIBRIERUNG messbar daneben — Training-Basisrate 37,8 %, live 53,2 %,
stabil über vier volle Tage. Alle unteren Klassen „zu niedrig".
· TRENNSCHÄRFE nicht entscheidbar — entkoppelt n=87, AUC 0,556,
95-%-KI [0,434 … 0,674] (enthält Zufall UND Messlatte).
Die Vermutung lautet deshalb: das Modell RANKT brauchbar, aber sein NIVEAU ist an
eine Basisrate genagelt, die wandert. Ein drittes Fitten derselben Bauart würde
daran nichts ändern — es würde nur einen neuen festen Achsenabschnitt lernen.
⚠ VORFRAGE, die über alles entscheidet: schwankt die Basisrate im Backtest-Zeitraum
überhaupt? Ist sie dort konstant, lässt sich das Problem an diesen Daten NICHT
studieren, und jede Variante wäre in-sample-Kosmetik. Wird zuerst geprüft und bei
Bedarf ausdrücklich gesagt.
GEPRÜFT WERDEN DREI VARIANTEN, alle auf derselben live-spiegelnden Stichprobe:
A) BASIS — 4 Merkmale, fester Achsenabschnitt (der heutige Stand)
B) +REGIME — 5. Merkmal: ATR relativ zum eigenen gleitenden Median
⚠ RELATIV, nicht absolut. Ein absoluter ATR-Schwellwert wäre
ein Selektions-Artefakt (Lehre aus `analyze_tageszeit.py` und
dem ORB-Fehlschlag) und würde über Regime hinweg nicht gelten.
C) ROLL-ACHSE — Merkmale wie A, aber der Achsenabschnitt wird laufend an die
zuletzt BEOBACHTETE Basisrate angepasst.
⚠ KAUSAL: ein Ausgang steht erst `_BRK_W` Bars später fest,
deshalb Nachlauf. Ohne den wäre es Look-ahead — genau der
Fehler, der am 06.08. Momentum und SIG scheinbar rettete.
⚠⚠ ENTSCHEIDUNGSREGEL, VORAB FESTGELEGT (sonst verschiebt sich hinterher die Latte):
Eine Variante gilt nur dann als besser, wenn sie in BEIDEN Fit-Richtungen
(H1→H2 UND H2→H1)
(1) den Kalibrierungsfehler senkt (mittlerer |vorhergesagt real| über die
Klassen, gewichtet nach n), UND
(2) die AUC NICHT verschlechtert (Toleranz 0,005), UND
(3) bei den Nachbar-Parametern nicht kippt (Fenster/Nachlauf).
Sonst: kein Einbau. Zwei Richtungen, weil eine einzelne Richtung schon einmal in
die Irre führte (`backtest_metalabel.py`, 05.08.).
"""
import sys
import numpy as np
import MetaTrader5 as mt5
from backtest_pbreak_retrain import (_ATRMIN, _BRK_W, _N_BARS, _atr_series,
_ema_series, auc, build_levels, collect,
fit_logreg, old_model)
from core.wave_rec import _EMA_FAST, _EMA_SLOW
_REG_FENSTER = 288 # gleitender ATR-Median (~1 Tag auf M5) für das Regime
_ROLL_N = 400 # wie viele zurückliegende Ausgänge die Achse nachführt
_ROLL_LAG = _BRK_W # Nachlauf: so lange braucht ein Ausgang, bis er feststeht
def live_model(feat):
"""Das HEUTE laufende Modell — Gewichte direkt aus `core/engine.py`, damit die
Kontrolle nicht von einer Kopie abhängt (Deployment-Drift wäre sonst genau
hier möglich: gemessen mit anderen Zahlen als betrieben)."""
from core.engine import _PB_MU, _PB_SD, _PB_W
z = _PB_W[0]
for k in range(4):
z += _PB_W[k + 1] * (feat[k] - _PB_MU[k]) / _PB_SD[k]
return 1 / (1 + np.exp(-z))
def kalib_fehler(p, y, kanten=(0.0, 0.2, 0.35, 0.5, 0.65, 1.01)):
"""Mittlerer |vorhergesagt real| über die Klassen, nach n gewichtet.
Das ist die Größe, die live danebenlag — nicht die AUC."""
ges = 0.0
n_ges = 0
for a, b in zip(kanten, kanten[1:]):
m = (p >= a) & (p < b)
k = int(m.sum())
if k < 20:
continue
ges += k * abs(p[m].mean() - y[m].mean())
n_ges += k
return ges / n_ges if n_ges else float("nan")
def zeig(titel, p, y):
print(f" {titel:<34} AUC {auc(p, y):.3f} Ø-Vorhersage {100*p.mean():>5.1f} % "
f"vs. real {100*y.mean():>5.1f} % Kalibr.-Fehler {100*kalib_fehler(p, y):>5.2f} Pp")
def regime_reihe(AT):
"""ATR relativ zum eigenen gleitenden Median — regime-normiert, damit der Wert
über verschiedene Vola-Niveaus dasselbe bedeutet. Kausal (nur Vergangenheit)."""
out = [1.0] * len(AT)
puffer = []
for i, a in enumerate(AT):
if a:
puffer.append(a)
if len(puffer) > _REG_FENSTER:
puffer.pop(0)
med = float(np.median(puffer))
out[i] = (a / med) if med > 0 else 1.0
return out
def roll_achse(p_roh, y, n_fenster=_ROLL_N, lag=_ROLL_LAG):
"""Verschiebt den Achsenabschnitt im Logit-Raum so, dass die mittlere
Vorhersage der zuletzt BEOBACHTETEN Basisrate folgt.
⚠ Kausal: für Zeile i zählen nur Ausgänge bis i-lag. Vor dem ersten vollen
Fenster bleibt die Vorhersage unverändert (kein Raten aus der Zukunft)."""
p = np.clip(p_roh, 1e-6, 1 - 1e-6)
logit = np.log(p / (1 - p))
out = np.array(p_roh, dtype=float)
for i in range(len(p)):
a = i - lag - n_fenster
b = i - lag
if a < 0 or b <= a:
continue
beob = y[a:b].mean()
erw = p_roh[a:b].mean()
if not (0 < beob < 1) or not (0 < erw < 1):
continue
# Differenz der Logits der MITTELWERTE = Niveau-Korrektur
delta = np.log(beob / (1 - beob)) - np.log(erw / (1 - erw))
out[i] = 1 / (1 + np.exp(-(logit[i] + delta)))
return out
def main():
n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000
mt5.initialize()
sym = next((c for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD")
if mt5.symbol_info(c)), None)
bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n)
mt5.shutdown()
H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars]
C = [float(b["close"]) for b in bars]
EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW)
AT = _atr_series(H, L, C); LV = build_levels(H, L)
REG = regime_reihe(AT)
mid = len(C) // 2
print("=" * 104)
print(f" P(break) — NIVEAU oder RANGFOLGE? {sym} M5, {len(C)} Bars")
print("=" * 104)
s1 = collect(_N_BARS, mid, H, L, C, EF, ES, AT, LV, REG)
s2 = collect(mid, len(C), H, L, C, EF, ES, AT, LV, REG)
if len(s1) < 500 or len(s2) < 500:
print("zu wenige Stichproben"); return
y1 = np.array([e["y"] for e in s1], float)
y2 = np.array([e["y"] for e in s2], float)
X1 = np.array([e["feat"] for e in s1]); X2 = np.array([e["feat"] for e in s2])
# ── VORFRAGE: wandert die Basisrate in diesen Daten ueberhaupt? ────────
print(f"\n H1 n={len(s1)} Basisrate {100*y1.mean():.1f} %")
print(f" H2 n={len(s2)} Basisrate {100*y2.mean():.1f} %")
print(f" Live gemessen (ab 01.08.): 53,2 % · ALT-Training: 37,8 %")
print("\n Basisrate in Zehnteln des Zeitraums (wandert sie?):")
alle_y = np.concatenate([y1, y2])
blk = len(alle_y) // 10
raten = [100 * alle_y[i*blk:(i+1)*blk].mean() for i in range(10)]
print(" " + " ".join(f"{r:5.1f}" for r in raten))
print(f" Spanne {min(raten):.1f}{max(raten):.1f} % "
f"(Schwankung {max(raten)-min(raten):.1f} Pp)")
if max(raten) - min(raten) < 8:
print(" ⚠ Die Basisrate ist in diesen Daten weitgehend STABIL — der live")
print(" beobachtete Drift (37,8 → 53,2 %) lässt sich hier nur begrenzt")
print(" nachstellen. Die Ergebnisse unten sind entsprechend zu lesen.")
# ── die drei Varianten, in BEIDEN Fit-Richtungen ───────────────────────
print("\n" + "=" * 104)
print(" VARIANTEN (vorab fixierte Regel: beide Richtungen besser kalibriert,")
print(" AUC nicht schlechter, Nachbarn kippen nicht)")
print("=" * 104)
for name, (Xa, ya, Xb, yb, sa, sb) in (
("FIT H1 → TEST H2", (X1, y1, X2, y2, s1, s2)),
("FIT H2 → TEST H1", (X2, y2, X1, y1, s2, s1))):
print(f"\n {name}")
w = fit_logreg(Xa, ya)
pb = 1 / (1 + np.exp(-(np.hstack([np.ones((len(Xb), 1)), Xb]) @ w)))
zeig("A) BASIS (fester Achsenabschnitt)", pb, yb)
# B) +Regime als 5. Merkmal
ra = np.array([[e["reg"]] for e in sa]); rb = np.array([[e["reg"]] for e in sb])
w5 = fit_logreg(np.hstack([Xa, ra]), ya)
p5 = 1 / (1 + np.exp(-(np.hstack([np.ones((len(Xb), 1)), Xb, rb]) @ w5)))
zeig("B) + Regime (ATR/Median)", p5, yb)
# C) rollierender Achsenabschnitt auf A
pc = roll_achse(pb, yb)
zeig(f"C) ROLL-ACHSE (n={_ROLL_N}, Nachlauf {_ROLL_LAG})", pc, yb)
# Nachbarn von C
for nf in (200, 800):
zeig(f" C-Nachbar n={nf}", roll_achse(pb, yb, n_fenster=nf), yb)
# ── ZWEI Kontrollen, und sie sind NICHT dasselbe ──────────────────
# ⚠ Beim ersten Lauf hatte ich `old_model` als „Modell wie live"
# beschriftet — falsch. `old_model` sind die Gewichte VOR dem
# Nachtrainieren (31.07.); live laufen seitdem `_PB_*` aus `engine.py`.
p_old = np.array([old_model(e["feat"]) for e in sb])
zeig("KONTROLLE 1: Modell VOR dem 31.07.", p_old, yb)
p_live = np.array([live_model(e["feat"]) for e in sb])
zeig("KONTROLLE 2: Modell wie HEUTE live", p_live, yb)
if __name__ == "__main__":
main()