Deployment-Drift: Stufe 1 (Trail-mult) + Stufe 2 (Entscheidungs-Telemetrie)

Behebt die strukturelle Schwachstelle, die heute dreimal zugeschlagen hat:
Komponenten werden unter anderen Bedingungen BETRIEBEN als VALIDIERT.
Track B schuetzt gegen Overfitting, nicht gegen Deployment-Drift.

STUFE 1 (core/trailing.py): _MULT_BY_TF durchgehend 1,5 (vorher M15 2,0 /
M30 2,5 / H1 3,0) und _MULT_BREAKOUT_ADD 0,5 -> 0,0. Die Staffelung war nie
gemessen; backtest_trailing.py validiert nur 1,5 und die breiteren Werte
sind dort messbar schlechter (80k Bars, 2 Halbjahre):
    mult 1,5  H1 SigmaR  -249 · H2 +1252 · Worst -1,50
    mult 2,0  H1 SigmaR  -412 · H2 +1187 · Worst -2,00
    mult 2,5  H1 SigmaR -1367 · H2 +1450 · Worst -2,50
Auf M30 fuhr der Bot also 5,5x schlechteres H1 und einen 67 % groesseren
Einzelverlust; bei Breakouts waren es 3,0 - nie getestet. Relevant, weil die
TF-Heuristik oft auf M15/M30 landet (31.07.: 19 von 33 Phasen).

STUFE 2a (wave_rec/history/engine): jede Empfehlung loggt einen
maschinenlesbaren block_reason - deadband, dead_hour, eia, htf_counter,
stretch, min_conf, breakout_pending, entry_room, no_data, stale. DB migriert
(Backup angelegt). Ohne den war nicht feststellbar, WELCHES der Gates die
93 % WARTEN erzeugt (Backtest-Erwartung ~43 %).

STUFE 2b (analyze_divergence.py, NEU): haelt vier Dinge gegen die
Backtest-Erwartung - A Signal-Verteilung, B Gate-Anteile inkl.
"im Backtest modelliert?", C TF-Wechsel-Rate, D0 Modell-Kalibrierung,
D Merkmals-Drift gegen _PB_MU/_SD.

LEHRE AUS DEM BAU SELBST: die Merkmals-Drift (D) haette den realen Fehler
NICHT gefangen - mom3 lag nur 0,50 Sigma unter mu, bei Gewicht 1,43 wurden
daraus aber -0,71 im Logit (23 % statt 41 % vorhergesagt). Deshalb ist D0
der schaerfere Test und die D-Schwelle auf 0,5 Sigma gesetzt.

Erster Lauf nach dem Fix bestaetigt das Nachtraining live:
D0 = 27,0 % vorhergesagt vs 28,0 % real (vorher 23 vs 41).

Stufe 3 (geteilter Exit-Kern) bleibt offen, spaeter und einzeln.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-07-31 12:54:34 +02:00
co-authored by Claude Opus 5
parent 0208101707
commit 97617cd81d
6 changed files with 322 additions and 17 deletions
+46
View File
@@ -1945,6 +1945,52 @@ Aktivierung genullt (kein Altwert-Fehlalarm); Erkennung erst ab dem 2. Modify. T
v=108. `GET /api/snapshot` ist ohnehin öffentlich/token-frei nutzbar — externe
Tools (Shortcuts, Home Assistant, …) können ihn direkt pollen.
## ⚠ Deployment-Drift — die strukturelle Schwachstelle (2026-07-31)
**Track B schützt gegen OVERFITTING** (ein Edge existiert gar nicht). Es schützt
**nicht** gegen **DEPLOYMENT-DRIFT**: ein real existierender Edge wird unter anderen
Bedingungen BETRIEBEN als VALIDIERT. An EINEM Tag wurden drei Fälle gefunden:
| # | Komponente | Gemessen unter | Betrieben unter | Schaden |
|---|---|---|---|---|
| 1 | P(break) | Anlauf zu **fixiertem** Level | **dynamisch** gewähltes Level | AUC 0,715 → **0,368** |
| 2 | `breakout_k=0,3` | **feste** Zeitebene | TF wechselt 33×/Tag, löscht den Anker | 43 % → **93 % WARTEN** |
| 3 | Trailing `mult` | **1,5** | **1,53,0** je nach TF | H1 ΣR 249 → **1367**, Worst 1,50 → 2,50 |
Keiner wäre durch MEHR Backtesting gefunden worden — die Backtests waren korrekt.
**Ursachen (strukturell):** (a) Backtests sind NACHBAUTEN, keine Nutzer des Live-Codes
(`sim_run`/`_ema_series`/`_atr_series` liegen in ≥6 Skripten als Kopie); (b) es gibt
**Live-only-Gates**, die kein Backtest modelliert (Entry-Raum, EIA, Dead-Hours,
News-Blackout, Cooldowns, Startup-Grace); (c) Live-Parameter sind **kontextabhängig**
(Trail-mult je TF, SL-ATR fix M15, TF selbst per Heuristik), Backtest-Parameter
konstant; (d) es gab **keine Telemetrie über die Entscheidung selbst**.
**Behebung in 3 Stufen (User-Entscheidung 2026-07-31):**
- **Stufe 1 — ERLEDIGT:** `_MULT_BY_TF` auf durchgehend **1,5** und
`_MULT_BREAKOUT_ADD` auf **0,0** (`core/trailing.py`) — der einzige gemessene Wert.
- **Stufe 2 — ERLEDIGT:** **Entscheidungs-Telemetrie.** (a) Jede Empfehlung loggt
jetzt einen maschinenlesbaren **`block_reason`** (`recommendations.block_reason`,
DB migriert mit Backup): `deadband · dead_hour · eia · htf_counter · stretch ·
min_conf · breakout_pending · entry_room · no_data · stale`; NULL = kein Block.
Ohne den war nicht feststellbar, WELCHES Gate die 93 % erzeugt. (b) **`analyze_
divergence.py`** hält vier Dinge gegen die Backtest-Erwartung: **A** Signal-Verteilung
(Soll ~43 % WARTEN), **B** Gate-Anteile inkl. Kennzeichnung „im Backtest modelliert?",
**C** TF-Wechsel-Rate (Soll: Median ≥15 min), **D0** Modell-Kalibrierung
(Ø-Vorhersage vs. echte Rate) und **D** Merkmals-Drift gegen `_PB_MU/_SD`.
**Lehre aus dem Bau selbst:** die Merkmals-Drift (D) hätte den realen Fehler
**NICHT** gefangen — mom3 lag nur **0,50σ** unter µ, bei Gewicht 1,43 wurden daraus
aber 0,71 im Logit (23 % statt 41 %). Deshalb ist **D0 der schärfere Test** und die
D-Schwelle auf 0,5σ gesetzt. Erster Lauf nach dem Fix: D0 zeigt **27,0 % vorhergesagt
vs. 28,0 % real** (vorher 23 vs 41) — die Reparatur ist damit live bestätigt.
- **Stufe 3 — OFFEN (später, einzeln):** geteilter Kern. Den Exit-Simulator EINMAL in
`core/exit_model.py` extrahieren und von `trailing.py` UND allen Backtests nutzen
lassen — Fall 3 wäre damit unmöglich gewesen. Gegenprüfung: bestehende Backtests
müssen danach dieselben Zahlen liefern.
**Erwartung: die drei Fälle sind nicht vollständig.** Sie wurden bei gezielter Suche
in ~20 min gefunden; die Trefferquote spricht für weitere. `analyze_divergence.py`
regelmäßig laufen lassen (Kandidat für den Wochenreport).
## Ausstehende Messungen — Erinnerung per Timer
`measurement_reminder.py` + Windows-Task **`OilMeasurementReminder`** (täglich 18:00)
+222
View File
@@ -0,0 +1,222 @@
#!/usr/bin/env python3
"""Divergenz-Wächter: läuft der Bot unter den Bedingungen, unter denen gemessen wurde?
Stufe 2 der Deployment-Drift-Behebung (2026-07-31).
WARUM ES DAS GIBT: An EINEM Tag wurden drei Fälle gefunden, in denen eine Komponente
unter anderen Bedingungen BETRIEBEN als VALIDIERT wurde:
1. P(break) — trainiert auf Anlauf zu FIXIERTEM Level, live dynamisch gewähltes
Level → AUC 0,715 fiel auf 0,368.
2. `breakout_k=0,3` — validiert auf FESTER Zeitebene, live wechselt sie 33×/Tag
und löschte dabei den Bestätigungs-Anker → 93 % WARTEN statt ~43 %.
3. Trailing-`mult` — validiert bei 1,5, live 1,53,0 je nach TF → H1 ΣR 249
statt 1367, Worst 1,50 statt 2,50.
Keiner davon wäre durch MEHR Backtesting gefunden worden — die Backtests waren
korrekt. Gefehlt hat die Kontrolle, ob der Betrieb ihren Annahmen entspricht.
Track B schützt gegen OVERFITTING (Edge existiert gar nicht). Dieses Skript schützt
gegen DEPLOYMENT-DRIFT (Edge existiert, läuft aber unter anderen Bedingungen).
GEPRÜFT WIRD:
A) Signal-Verteilung live vs. Backtest-Erwartung (`backtest_dist.py`: ~43 % WARTEN)
B) WELCHES Gate blockt — Anteile je `block_reason` (seit 2026-07-31 geloggt)
C) TF-Wechsel-Rate (die Bedingung, unter der `breakout_k` validiert wurde, ist
„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)
"""
import re
import sqlite3
import sys
from datetime import datetime, timedelta
DB = "oil_widget_history.db"
LOG = "oil_widget.log"
# Erwartungswerte aus den Backtests — die Messlatte
_EXP_WAIT = 43.0 # backtest_dist.py: ~57 % LONG/SHORT, ~43 % WARTEN
_EXP_WAIT_TOL = 15.0 # ab dieser Abweichung wird geflaggt (Pp)
def _flag(ok: bool) -> str:
return "OK " if ok else ""
def sec_a(c, t0):
print("\n" + "=" * 84)
print(" A) SIGNAL-VERTEILUNG — läuft der Bot so oft wie gemessen?")
print("=" * 84)
c.execute("SELECT signal, COUNT(*) FROM recommendations WHERE timestamp>=? "
"GROUP BY signal", (t0,))
d = dict(c.fetchall()); tot = sum(d.values())
if not tot:
print(" keine Daten"); return
w = 100.0 * d.get("WARTEN", 0) / tot
dev = w - _EXP_WAIT
print(f" n={tot} WARTEN {w:.1f} % LONG {100*d.get('LONG',0)/tot:.1f} % "
f"SHORT {100*d.get('SHORT',0)/tot:.1f} %")
print(f" Backtest-Erwartung: {_EXP_WAIT:.0f} % WARTEN → Abweichung {dev:+.1f} Pp "
f"{_flag(abs(dev) <= _EXP_WAIT_TOL)}")
if dev > _EXP_WAIT_TOL:
print(" ⚠ Der Bot wartet deutlich öfter als gemessen — ein Live-Gate blockt")
print(" mehr, als der Backtest kennt. Siehe Abschnitt B.")
def sec_b(c, t0):
print("\n" + "=" * 84)
print(" B) WELCHES GATE BLOCKT? (block_reason, seit 2026-07-31)")
print("=" * 84)
try:
c.execute("SELECT block_reason, COUNT(*) FROM recommendations "
"WHERE timestamp>=? AND signal='WARTEN' GROUP BY block_reason "
"ORDER BY COUNT(*) DESC", (t0,))
rows = c.fetchall()
except sqlite3.OperationalError:
print(" Spalte block_reason fehlt (ältere DB)"); return
tot = sum(n for _, n in rows)
if not tot:
print(" noch keine Daten — das Logging läuft erst seit dem Neustart am 31.07.")
return
# Welche Gates kennt der Backtest? (nur diese dürfen den Vergleich prägen)
im_backtest = {"deadband": True, "htf_counter": True, "stretch": True,
"min_conf": True, "breakout_pending": True,
"entry_room": False, "dead_hour": False, "eia": False,
"no_data": False, "stale": False, None: None}
print(f" {'Gate':<20}{'n':>8}{'Anteil':>9} im Backtest modelliert?")
for br, n in rows:
mb = im_backtest.get(br)
mark = "ja" if mb else ("NEIN — live-only" if mb is False else "")
print(f" {str(br):<20}{n:>8}{100*n/tot:>8.1f} % {mark}")
live_only = sum(n for br, n in rows if im_backtest.get(br) is False)
print(f"\n Nur-Live-Gates blocken {100*live_only/tot:.1f} % aller WARTEN-Fälle.")
print(" ⚠ Dieser Anteil ist per Definition NICHT durch einen Backtest gedeckt.")
def sec_c(t0):
print("\n" + "=" * 84)
print(" C) TF-WECHSEL — `breakout_k` wurde auf FESTER Zeitebene validiert")
print("=" * 84)
try:
sw = []
for ln in open(LOG, encoding="utf-8", errors="ignore"):
m = re.search(r"^(\S+ \S+).*Wellen-Timeframe → (\w+)", ln)
if m:
try:
t = datetime.strptime(m.group(1), "%Y-%m-%d %H:%M:%S")
except ValueError:
continue
if t.timestamp() >= t0:
sw.append((t, m.group(2)))
except FileNotFoundError:
print(" kein Log gefunden"); return
if not sw:
print(" keine TF-Wechsel im Zeitraum OK"); return
days = max(1.0, (datetime.now().timestamp() - t0) / 86400)
gaps = sorted((sw[i][0] - sw[i - 1][0]).total_seconds() for i in range(1, len(sw)))
med = gaps[len(gaps) // 2] / 60 if gaps else 0
print(f" {len(sw)} Wechsel in {days:.1f} Tagen = {len(sw)/days:.1f}/Tag")
if gaps:
print(f" Median-Abstand {med:.0f} min {_flag(med >= 15)}")
if med < 15:
print(" ⚠ Wechselt schneller, als ein Signal seine Breakout-Bestätigung")
print(" abschließen kann → prüfe `tf_min_dwell_s`.")
def sec_d0(c, t0):
"""Der SCHÄRFSTE Test: sagt das Modell im Mittel das voraus, was eintritt?
⚠ Diese Prüfung ersetzt NICHT die Merkmals-Drift unten, sie ist ihr überlegen —
und das ist eine Lehre aus dem Bau dieses Skripts: mit den ALTEN Modellwerten
lag `mom3` nur **0,50σ** unter µ und wäre an einer 1σ-Schwelle NICHT
aufgefallen. Entscheidend ist nicht die Abweichung des Merkmals, sondern ihre
WIRKUNG nach Gewichtung (mom3 hatte Gewicht 1,43 → 0,50σ wurden zu 0,71 im
Logit). Der Vergleich „Ø-Vorhersage vs. echte Rate" fängt das direkt ab: er
hätte 23 % vs 41 % sofort gezeigt.
"""
print("\n" + "=" * 84)
print(" D0) MODELL-KALIBRIERUNG — sagt es voraus, was eintritt?")
print("=" * 84)
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 — zu wenig"); 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).")
def sec_d(c, t0):
print("\n" + "=" * 84)
print(" D) MERKMALS-DRIFT der eingebetteten Modelle")
print("=" * 84)
try:
from core.engine import _PB_MU, _PB_SD
except Exception as e:
print(f" Modell nicht ladbar: {e}"); return
# Live-Merkmale liegen (noch) nicht in der DB → aus candles_m1 rekonstruieren
c.execute("SELECT MAX(time) FROM candles_m1")
last = c.fetchone()[0]
if not last:
print(" keine M1-Kerzen — Rekonstruktion nicht möglich"); return
import time as _t
off = round((last - _t.time()) / 3600) * 3600
c.execute("""SELECT ts,direction,level,price_at_pred,atr FROM pbreak_predictions
WHERE ts>=? AND atr IS NOT NULL""", (t0,))
rows = c.fetchall()
if len(rows) < 30:
print(f" nur {len(rows)} Vorhersagen im Zeitraum — zu wenig"); return
def close_at(ts):
c.execute("SELECT c FROM candles_m1 WHERE time<=? ORDER BY time DESC LIMIT 1", (ts,))
r = c.fetchone(); return r[0] if r else None
m3, m6 = [], []
for ts, d, lvl, px, atr in rows:
b = ts + off
c0, c3, c6 = close_at(b), close_at(b - 15 * 60), close_at(b - 30 * 60)
if None in (c0, c3, c6):
continue
dd = 1 if d == "LONG" else -1
m3.append((c0 - c3) * dd / atr); m6.append((c0 - c6) * dd / atr)
if len(m3) < 30:
print(" zu wenige rekonstruierbare Merkmale"); return
print(f" n={len(m3)} (P(break)-Modell, Merkmale aus candles_m1 rekonstruiert)")
print(f" {'Merkmal':<8}{'Live-µ':>10}{'Modell-µ':>11}{'Δ in SD':>10}")
for nm, v, k in (("mom6", m6, 0), ("mom3", m3, 1)):
mean = sum(v) / len(v)
dsd = (mean - _PB_MU[k]) / _PB_SD[k]
print(f" {nm:<8}{mean:>10.3f}{_PB_MU[k]:>11.3f}{dsd:>9.2f}σ {_flag(abs(dsd) <= 0.5)}")
print("\n ⚠ Schwelle bewusst 0,5σ (nicht 1σ): der reale P(break)-Fehler lag bei")
print(" mom3 nur 0,50σ unter µ — bei Gewicht 1,43 wurden daraus aber 0,71 im")
print(" Logit, was 23 % statt 41 % vorhersagte. Merkmals-Drift allein")
print(" unterschätzt den Schaden; D0 (Kalibrierung) ist der schärfere Test.")
def main():
days = int(sys.argv[1]) if len(sys.argv) > 1 else 7
t0 = (datetime.now() - timedelta(days=days)).timestamp()
db = sqlite3.connect(DB); c = db.cursor()
print("=" * 84)
print(f" DIVERGENZ-WÄCHTER — läuft der Bot unter den validierten Bedingungen?")
print(f" Zeitraum: letzte {days} Tage")
print("=" * 84)
sec_a(c, t0)
sec_b(c, t0)
sec_c(t0)
sec_d0(c, t0)
sec_d(c, t0)
db.close()
print("\n" + "=" * 84)
print(" Track B schützt gegen Overfitting. DIESER Report schützt gegen")
print(" Deployment-Drift: gemessen unter X, betrieben unter Y.")
print("=" * 84)
if __name__ == "__main__":
main()
+5 -1
View File
@@ -1366,7 +1366,11 @@ class TradingEngine:
reversal=("rev" if "REV" in (rec.get("setup") or "") else None),
ai_sentiment=adv.get("bias"), ai_confidence=adv.get("confidence"),
setup=rec.get("setup"), regime=None, rsi=s.get("rsi_m15"),
news_score=_news)
news_score=_news,
# Stufe-2-Telemetrie (2026-07-31): WELCHES Gate blockt gerade?
# Ohne das war live nicht feststellbar, warum der Bot zu 93 %
# WARTEN sagt (Backtest-Erwartung ~43 %).
block=rec.get("block"))
except Exception as e:
log.warning(f"log_recommendation: {e}")
+12 -5
View File
@@ -85,7 +85,13 @@ SCHEMA = [
setup TEXT,
regime TEXT,
rsi REAL,
news_score REAL
news_score REAL,
-- Maschinenlesbarer Grund für WARTEN (Stufe-2-Telemetrie 2026-07-31):
-- deadband · dead_hour · eia · htf_counter · stretch · min_conf ·
-- breakout_pending · entry_room · no_data · stale. NULL = kein Block.
-- Ohne den war live nicht feststellbar, WELCHES Gate die 93 % WARTEN
-- erzeugt (Backtest-Erwartung ~43 %).
block_reason TEXT
)
""",
"""
@@ -340,7 +346,8 @@ class HistoryLogger:
setup: str | None = None,
regime: str | None = None,
rsi: float | None = None,
news_score: float | None = None):
news_score: float | None = None,
block: str | None = None):
"""
Empfehlungs-Logging mit Throttling: nur jede N Sekunden, um die DB
nicht mit identischen Snapshots zu fluten.
@@ -355,13 +362,13 @@ class HistoryLogger:
(timestamp, signal, score, conf_pct,
angle_m5, angle_m15, angle_m30, angle_h1,
reversal, ai_sentiment, ai_confidence,
setup, regime, rsi, news_score)
VALUES (?,?,?,?, ?,?,?,?, ?,?,?, ?,?,?,?)
setup, regime, rsi, news_score, block_reason)
VALUES (?,?,?,?, ?,?,?,?, ?,?,?, ?,?,?,?,?)
""", (now, signal, score, conf_pct,
angles.get("M5"), angles.get("M15"),
angles.get("M30"), angles.get("H1"),
reversal, ai_sentiment, ai_confidence,
setup, regime, rsi, news_score))
setup, regime, rsi, news_score, block))
conn.commit()
def log_signal(self, *, signal_type: str, direction: str,
+21 -6
View File
@@ -107,16 +107,31 @@ _TF_LABELS = {
mt5.TIMEFRAME_H1: "H1",
}
# Trail-Multiplikator je Timeframe: niedrige TF (Scalp) → enger Stop,
# hohe TF (Trend) → mehr Puffer. Ersetzt die alte 2.54.5-Logik.
# Trail-Multiplikator je Timeframe.
# ⚠⚠ ANGEGLICHEN AUF DEN GEMESSENEN WERT 1,5 (2026-07-31, Stufe 1 der
# Deployment-Drift-Behebung). VORHER: M15 2,0 · M30 2,5 · H1 3,0 (+0,5 bei
# Breakouts) — diese Staffelung war **nie gemessen**. `backtest_trailing.py`
# validiert ausschließlich 1,5, und die breiteren Werte sind dort MESSBAR
# SCHLECHTER (80k Bars, 2 Halbjahre, Signal Trend+Reversal):
# mult 1,5 (start 0,3): H1 ΣR 249 · H2 +1252 · Worst 1,50
# mult 2,0 (start 0,3): H1 ΣR 412 · H2 +1187 · Worst 2,00
# mult 2,5 (start 0,3): H1 ΣR 1367 · H2 +1450 · Worst 2,50
# Auf M30 fuhr der Bot also 5,5× schlechteres H1 und einen 67 % größeren
# Einzelverlust; mit `_MULT_BREAKOUT_ADD` waren es bei Breakouts sogar 3,0 —
# ein Wert, den nie jemand getestet hat. Real relevant, weil die TF-Heuristik
# häufig auf M15/M30 landet (31.07.: 19 von 33 Phasen).
# ⚠ Falls die Staffelung je zurück soll: erst `backtest_trailing.py` auf
# M15-/M30-BASIS-Signalen laufen lassen — bisher misst er nur M5.
_MULT_BY_TF = {
mt5.TIMEFRAME_M1: 1.5,
mt5.TIMEFRAME_M5: 1.5,
mt5.TIMEFRAME_M15: 2.0,
mt5.TIMEFRAME_M30: 2.5,
mt5.TIMEFRAME_H1: 3.0,
mt5.TIMEFRAME_M15: 1.5,
mt5.TIMEFRAME_M30: 1.5,
mt5.TIMEFRAME_H1: 1.5,
}
_MULT_BREAKOUT_ADD = 0.5 # Breakouts brauchen etwas mehr Luft (gedeckelt)
# 0,0 statt 0,5: der Aufschlag hob Breakout-Trades auf mind. 2,0 — gemessen
# schlechter als 1,5 (s. o.) und für Breakouts nie separat validiert.
_MULT_BREAKOUT_ADD = 0.0
_PHASE_RANK = {"Init": 0, "Trail": 1, "Lock": 2}
+16 -5
View File
@@ -564,7 +564,7 @@ class WaveRecommender:
snap["breakout"] = {"pending": True, "dir": sig,
"level": round(p["level"], 3), "need": round(need, 3)}
return {"signal": "WARTEN", "conf_pct": 0, "score": 0.0, "setup": "WAVE",
"regime": None, "rsi": None,
"regime": None, "rsi": None, "block": "breakout_pending",
"reasons": [f"warte auf Breakout (+{k:.1f}×ATR {sig}, noch {need:.2f})"]}
def _room_gate(self, rec, cur):
@@ -594,7 +594,7 @@ class WaveRecommender:
if dist >= x:
return rec
return {"signal": "WARTEN", "conf_pct": 0, "score": 0.0, "setup": "WAVE",
"regime": None, "rsi": None,
"regime": None, "rsi": None, "block": "entry_room",
"reasons": [f"kein Raum: {'Widerstand' if d > 0 else 'Support'} "
f"{lvl:.2f} nur {dist:.2f}×ATR entfernt (Gate {x:.1f}) — "
f"Ertrag gedeckelt, Kosten fressen den Edge"]}
@@ -673,11 +673,16 @@ class WaveRecommender:
"dir": "LONG" if stretch < 0 else "SHORT"}
else:
snap["bounce"] = {"state": None, "dir": None}
wait = {"signal": "WARTEN", "conf_pct": 0, "score": 0.0,
# `block` = maschinenlesbarer Grund, WARUM WARTEN (Stufe-2-Telemetrie
# 2026-07-31). Die `reasons` sind für Menschen, `block` fuer die Statistik:
# ohne ihn war live nicht feststellbar, welches der Gates die 93 % WARTEN
# erzeugt (Backtest-Erwartung ~43 %).
wait = {"signal": "WARTEN", "conf_pct": 0, "score": 0.0, "block": None,
"setup": "WAVE", "regime": None, "rsi": None, "reasons": []}
# Totband: kein klarer Trend → kein Trade (Chop)
if abs(sep) < _TREND_DEADBAND:
wait["block"] = "deadband"
wait["reasons"] = [f"kein klarer Trend ({tf_lbl}, EMA-Abstand {sep:+.2f}×ATR)"]
return wait, snap
@@ -685,6 +690,7 @@ class WaveRecommender:
# Kostenfalle + 12/16 Uhr. Leer = AUS, User-Vorgabe 2026-07-22 trotz Messung).
if hour is not None and hour in self._dead_hours:
why = "Nacht-Spread frisst den Edge" if hour <= 7 else "gemessen negativer Edge"
wait["block"] = "dead_hour"
wait["reasons"] = [f"Zeit-Gate {hour}:00 Uhr — {why}, kein Trade"]
return wait, snap
@@ -695,6 +701,7 @@ class WaveRecommender:
if hour is not None:
_now_b = datetime.now(_BERLIN)
if _now_b.weekday() == 2 and (15, 30) <= (_now_b.hour, _now_b.minute) < (16, 30):
wait["block"] = "eia"
wait["reasons"] = ["EIA-Blackout Mi 15:3016:30 — Lagerdaten-Vorlauf, "
"gemessen negativ, kein Trade"]
return wait, snap
@@ -721,15 +728,18 @@ class WaveRecommender:
# Higher-TF-Gegen-Trend-Filter: kein Short im M30-Aufwärtstrend (und
# umgekehrt). Per Backtest belegt (Ø-Edge ×2). Nur reguläre Trendsignale.
if htf_trend != 0 and htf_trend != d_sig:
wait["block"] = "htf_counter"
wait["reasons"] = [
f"gegen {_HTF_LABEL}-Trend "
f"({'auf' if htf_trend > 0 else 'ab'}) — kein Gegen-Trade"]
return wait, snap
# Anti-Überdehnung: nicht weit weg von der EMA hinterherkaufen/-shorten
if sig == "LONG" and stretch > _STRETCH_MAX:
wait["block"] = "stretch"
wait["reasons"] = [f"überdehnt: {stretch:+.1f}×ATR über EMA — kein Spät-Long"]
return wait, snap
if sig == "SHORT" and stretch < -_STRETCH_MAX:
wait["block"] = "stretch"
wait["reasons"] = [f"überdehnt: {stretch:+.1f}×ATR unter EMA — kein Spät-Short"]
return wait, snap
@@ -830,6 +840,7 @@ class WaveRecommender:
# Mindest-Konfidenz-Gate: schwache Setups (zu viele Strafen gestapelt)
# tragen negativen Edge (gemessen Band 4054 %) → kein Trade.
if conf < _MIN_CONF:
wait["block"] = "min_conf"
wait["reasons"] = [f"Konfidenz {conf}% < {_MIN_CONF}% — Setup zu schwach"] + reasons[:2]
return wait, snap
@@ -848,11 +859,11 @@ class WaveRecommender:
rec, ts, err = self._rec, self._ts, self._error
if rec is None:
return {"signal": "WARTEN", "conf_pct": 0, "score": 0.0,
"setup": "WAVE", "regime": None, "rsi": None,
"setup": "WAVE", "regime": None, "rsi": None, "block": "no_data",
"reasons": [err or "keine Wellen-Daten"]}
if not ts or time.time() - ts > _STALE_S:
return {"signal": "WARTEN", "conf_pct": 0, "score": 0.0,
"setup": "WAVE", "regime": None, "rsi": None,
"setup": "WAVE", "regime": None, "rsi": None, "block": "stale",
"reasons": ["Wellen-Daten veraltet"]}
return dict(rec)