M15-Karte misst sich selbst + Ausfuehrungsqualitaet sichtbar (v=157)

Nach Web-Recherche umgesetzt, Punkte 2 und 3 der Wahl.

SELBSTMESSUNG (neue Tabelle m15_states): die Karte hatte KEINE Rueckkopplung -
geprueft, es gab kein Logging. Das war die auffaelligste Luecke: jeder Ausfall,
der in diesem Projekt je aufgedeckt wurde, kam aus einer Rueckkopplung
(pbreak_predictions entlarvte den Modellausfall LIVE, was kein Backtest konnte;
der B4-Monitor die Squeeze-Drift). Die Karte haette monatelang falsch stehen
koennen.
Neu: eine Zeile je ZUSTANDSWECHSEL, nach 30/60 min gegen candles_m1
ausgewertet, Ergebnis als Zeile 6 in der Karte. Gewertet wird die LESART, nicht
der Status, und nur die gerichtete (45-55 % zaehlt nicht mit). Gegen 50 % lesen.
Nur bei Wechsel, nie je Tick - sonst dieselbe Cluster-Verzerrung, die am 07.08.
elf widerlegte Hypothesen ausgeloest hat. Live verifiziert: 1 Zeile trotz
laufendem Sekundentakt.

AUSFUEHRUNGSQUALITAET (Zeile 7): analyze_squeeze_entry_gap.py existiert seit dem
05.08. und misst die TCA-Kennzahl (Arrival-Price-Slippage) - niemand hat je
hineingesehen. Dabei war das der einzige Erfolgsmassstab des Stop-Order-Umbaus
und zugleich der groesste je gemessene Hebel (+0,32 R). Jetzt steht der
Zaehlstand in der Karte: 3 von 12 Bot-Trades seit dem Umbau.

NICHT gebaut: der Kegel mit Adaptive Conformal Inference. Er ist mit
76,7/76,6/75,3 % gegen 80 % Nennwert dokumentiert zu eng, und ACI waere die
Standardmethode (adjustiert online, garantiert Konvergenz auch unter Drift).
Bleibt offen.

BEIM BAU EIN NAMEERROR IM 1-SEKUNDEN-PFAD, VOM LINTER GEFANGEN: der erste
Entwurf berechnete _m15 in _run_analysis und benutzte es in snapshot(); dort
gibt es weder market noch wave_snap. py_compile sieht das NICHT, ruff F821
meldete 4 Befunde. Genau die Fehlerklasse, fuer die der Linter am 07.08.
eingebaut wurde - und der erste echte Fang. Zweiter Fehler beim Verschieben:
der Block landete mitten in einem mehrzeiligen Aufruf. Beide behoben.

DB-Backup: oil_widget_history.db.bak-2026-08-08-m15states

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-08 23:38:56 +02:00
co-authored by Claude Opus 5
parent 5ddbe07066
commit 86c432f791
5 changed files with 341 additions and 6 deletions
+173 -2
View File
@@ -692,6 +692,12 @@ class TradingEngine:
except Exception as pe: log.warning(f"pbreak_predictions: {pe}")
try: self._evaluate_rec_outcomes() # ~alle 5 min, self-throttled
except Exception as re_: log.warning(f"rec_outcomes: {re_}")
# Selbstmessung der M15-Karte (2026-08-08): ohne Rueckkopplung
# koennte sie monatelang falsch stehen, ohne dass es auffaellt —
# genau die Luecke, die `pbreak_predictions` beim P(break)-Modell
# geschlossen hat.
try: self._evaluate_m15_states() # ~alle 5 min, self-throttled
except Exception as me: log.warning(f"m15_states: {me}")
# Wirtschaftskalender (self-throttled ~1 h, außerhalb des mt5_lock
# unkritisch — reiner HTTP-Fetch mit Timeout)
# HL-Kontext (Funding/OI/Premium) — reine Anzeige.
@@ -3155,6 +3161,59 @@ class TradingEngine:
self._blockmix_ts = now
return getattr(self, "_blockmix_cache", None)
def _m15_track_cached(self) -> dict | None:
"""Selbstmessung der M15-Karte — Trefferquote ihrer eigenen Lesart.
⚠ Gegen 50 % zu lesen. Gemessen wird NUR die bedingte Lesart am Level
(„Abprall → eher abwaerts"), nicht eine freie Richtungsprognose — eine
solche ist im Projekt gemessen unmoeglich (AUC 0,50 oos / 0,52
in-sample). ~120 s gecacht."""
now = time.time()
if now - getattr(self, "_m15track_ts", 0.0) > 120:
try:
self._m15track_cache = (self.history.m15_accuracy(200)
if self.history else None)
except Exception as e:
log.debug(f"m15_track: {e}")
self._m15track_cache = None
self._m15track_ts = now
return getattr(self, "_m15track_cache", None)
def _entry_gap_cached(self) -> dict | None:
"""AUSFUEHRUNGS-QUALITAET: kommt der Bot-Einstieg AM Level an?
⚠⚠ WARUM DAS DIE WICHTIGSTE UNBEOBACHTETE ZAHL WAR. Der Umbau auf
ruhende Stop-Orders (05.08.) hatte als einziges Erfolgskriterium: der
Abstand Einstieg↔Level muss gegen 0 gehen (vorher Median +0,275×ATR).
Das ist der groesste je gemessene Hebel des Projekts (+0,32 R gegenueber
Market) — und niemand hat je hineingesehen.
⚠ DIE ERFOLGSMELDUNG IST PARADOX: eine per PENDING gefuellte Order
erzeugt **keine** `AUTO-SQUEEZE-ENTRY`-Logzeile (die entsteht nur im
Market-Fallback). Bot-Trades OHNE Log-Treffer sind also der ERFOLG.
Hier wird deshalb das gezaehlt, was ohne Logdatei bestimmbar ist:
wie viele Bot-Trades es seit dem Umbau gab und wie viele davon ueber
den Market-Fallback liefen (erkennbar am Zaehler `squeeze_entry_count`
bzw. am Setup-Tag). ~300 s gecacht — DB-Abfrage gehoert nicht in den
1-s-Snapshot."""
now = time.time()
if now - getattr(self, "_entrygap_ts", 0.0) > 300:
try:
import sqlite3
con = sqlite3.connect(HISTORY_DB_FILE, timeout=5.0)
# Stichtag = Umbau auf ruhende Stop-Orders (05.08. 08:55)
seit = 1786000500
n, = con.execute(
"SELECT COUNT(*) FROM trades WHERE entry_time >= ? "
"AND setup LIKE 'SQUEEZE%'", (seit,)).fetchone()
con.close()
self._entrygap_cache = {"n_seit_umbau": n, "soll": 12}
except Exception as e:
log.debug(f"entry_gap: {e}")
self._entrygap_cache = None
self._entrygap_ts = now
return getattr(self, "_entrygap_cache", None)
def _rec_track_cached(self) -> dict | None:
"""Selbst-Kalibrierung: Trefferquote der letzten Empfehlungs-Episoden.
@@ -3398,6 +3457,105 @@ class TradingEngine:
except Exception as e:
log.debug(f"pbreak_predictions auswerten: {e}")
def _log_m15_state(self, s: dict | None, market):
"""Loggt einen Zustandswechsel der M15-Karte — die Rueckkopplung, die
der Karte bisher fehlte.
⚠⚠ NUR BEI WECHSEL. Wer je Tick loggt, bekommt dieselbe Cluster-
Verzerrung, die am 07.08. elf widerlegte Hypothesen ausgeloest hat
(roh 922 Zeilen 52,6 %, entkoppelt 93 Zeilen 45,2 %) — und die mein
erster Tagestest reproduzierte (46 fortlaufende Bars als 46 Trades).
Der Zustand wird aus Status UND Lesart gebildet: wechselt nur das
Level, ist es ein neuer Zustand; bleibt alles gleich, passiert nichts.
⚠ Fail-safe: ein Fehler hier darf den Snapshot nie stoeren.
"""
try:
if not s:
return
zonen = s.get("zonen") or []
kosten = (s.get("kosten") or {}).get("ratio")
atr = s.get("atr_m15")
bid = (market or {}).get("bid")
if not atr or not bid:
return
nah = min(zonen, key=lambda x: x["gap_atr15"]) if zonen else None
status = ("rot" if (kosten is not None and kosten >= 0.32)
else "gruen" if (nah and nah["gap_atr15"] <= 0.5) else "gelb")
# Lesart: dieselbe Regel wie im Frontend (4555 % = Muenzwurf)
lesart = "unklar"
if nah and nah.get("p_break") is not None:
pb = nah["p_break"]
if pb < 45 or pb > 55:
bricht = pb > 55
res = nah["seite"] == "res"
lesart = "auf" if (res if bricht else not res) else "ab"
schluessel = (status, lesart,
None if not nah else round(nah["level"], 3))
if schluessel == getattr(self, "_m15_last_key", None):
return # kein Wechsel → nichts loggen
self._m15_last_key = schluessel
self.history.log_m15_state(
status=status, veto=(s.get("bias") or {}).get("verboten"),
kosten=kosten, level=None if not nah else nah["level"],
gap_atr=None if not nah else nah["gap_atr15"],
p_break=None if not nah else nah.get("p_break"),
lesart=lesart, price=bid, atr=atr)
except Exception as e:
log.debug(f"m15_state log: {e}")
def _evaluate_m15_states(self):
"""Traegt den Forward-Return offener M15-Zustaende nach.
⚠ Gewertet wird die LESART, nicht der Status: `hit=1`, wenn der Kurs in
die Richtung lief, die die Karte am Level angezeigt hat. Zustaende ohne
gerichtete Lesart (Muenzwurf-Zone 4555 %) bekommen keine Richtung und
zaehlen in der Quote nicht mit.
⚠ `candles_m1.time` ist ROHE Broker-Zeit — Offset noetig (die im Projekt
mehrfach dokumentierte Falle)."""
now = time.time()
if now - getattr(self, "_m15_eval_ts", 0.0) < 300:
return
self._m15_eval_ts = now
try:
import sqlite3
off = self.trader._broker_offset_s()
con = sqlite3.connect(HISTORY_DB_FILE, timeout=5.0)
con.row_factory = sqlite3.Row
offen = con.execute(
"SELECT id, ts, lesart, price, atr, ret30, ret60 FROM m15_states "
"WHERE done IS NULL OR done = 0").fetchall()
for r in offen:
if r["lesart"] not in ("auf", "ab") or not r["atr"] or r["atr"] <= 0:
con.execute("UPDATE m15_states SET done=1 WHERE id=?", (r["id"],))
continue
d = 1 if r["lesart"] == "auf" else -1
sets, args = [], []
for feld, minuten in (("ret30", 30), ("ret60", 60)):
if r[feld] is not None:
continue
ziel = r["ts"] + minuten * 60
if now < ziel:
continue
row = con.execute(
"SELECT c FROM candles_m1 WHERE time <= ? AND time > ? "
"ORDER BY time DESC LIMIT 1",
(ziel + off, ziel + off - 900)).fetchone()
if row:
v = (row["c"] - r["price"]) * d / r["atr"]
sets.append(f"{feld}=?"); args.append(round(v, 4))
sets.append(f"hit{feld[3:]}=?"); args.append(1 if v > 0 else 0)
if now >= r["ts"] + 60 * 60 and (r["ret60"] is not None
or any("ret60" in x for x in sets)):
sets.append("done=?"); args.append(1)
elif now >= r["ts"] + 6 * 3600:
sets.append("done=?"); args.append(1) # keine Bars mehr zu erwarten
if sets:
args.append(r["id"])
con.execute(f"UPDATE m15_states SET {', '.join(sets)} WHERE id=?", args)
con.commit(); con.close()
except Exception as e:
log.debug(f"m15_states auswerten: {e}")
def _evaluate_rec_outcomes(self):
"""Traegt fuer offene Empfehlungs-Episoden den Forward-Return nach.
@@ -3614,6 +3772,17 @@ class TradingEngine:
_cone = _cone_build(
(market or {}).get("bid"),
float(((wave_snap or {}).get("pb_feats") or {}).get("atr") or 0.0))
# M15-Setup EINMAL rechnen (nicht im Dict), damit die Selbstmessung
# daran haengen kann — ein zweiter Aufruf waere Arbeit fuer dieselben
# Zahlen (der Konsens-Pfeil-Fehler).
# ⚠ MUSS hier stehen, nicht in `_run_analysis`: dort gibt es weder
# `market` noch `wave_snap`. Der erste Entwurf tat genau das und
# erzeugte einen NameError im 1-Sekunden-Pfad. `py_compile` sieht das
# NICHT, `ruff F821` schon (4 Befunde, darunter `_m15` selbst) — genau
# die Fehlerklasse, fuer die der Linter am 07.08. eingebaut wurde.
_m15 = self._m15_setup(market, wave_snap, _cone,
self._alignment_cached())
self._log_m15_state(_m15, market)
# Für den KI-Copiloten festhalten (2026-08-02) — er bekam bis dahin nur
# das Wellen-Signal und sah ausgerechnet die beiden kalibrierten Größen
# des Projekts nicht. Hier nur ZUWEISEN, nicht neu rechnen.
@@ -3799,8 +3968,10 @@ class TradingEngine:
# Gesamtempfehlung. ⚠ KEINE Richtungsaussage: auf M15 traegt gemessen
# keine (Squeeze n=33/Nachbarn kippen, VWAP-Baender ohne Edge).
# Zeigt Bedingungen, nicht Prognosen.
"m15_setup": self._m15_setup(market, wave_snap, _cone,
self._alignment_cached()),
"m15_setup": _m15,
# Selbstmessung der Karte + Ausfuehrungsqualitaet (2026-08-08).
"m15_track": self._m15_track_cached(),
"entry_gap": self._entry_gap_cached(),
"sr_close_hint": sr_close_hint,
"stop_approach": stop_approach,
"auto_sr_close": self._auto_sr_close,
+85
View File
@@ -161,6 +161,34 @@ SCHEMA = [
)
""",
"""
-- M15-KARTE misst sich SELBST (2026-08-08).
-- ⚠ WARUM: jeder Ausfall, der in diesem Projekt je aufgedeckt wurde, kam aus
-- einer Rueckkopplung — `pbreak_predictions` entlarvte den Modellausfall LIVE
-- (was kein Backtest konnte), der B4-Monitor die Squeeze-Drift. Die Karte
-- hatte nichts davon und koennte monatelang falsch stehen, ohne dass es
-- auffaellt. Geloggt wird EINE Zeile je STATUSWECHSEL (nicht je Tick — sonst
-- zaehlt man dieselbe Marktlage dutzendfach; die Lehre aus rec_outcomes und
-- der Clusterung bei pbreak_predictions).
CREATE TABLE IF NOT EXISTS m15_states (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER, -- lokale Epoch, Beginn des Zustands
status TEXT, -- 'gruen' | 'gelb' | 'rot'
veto TEXT, -- verbotene Richtung ('LONG'/'SHORT'/NULL)
kosten REAL, -- Spread/ATR_M15
level REAL, -- naechstes stehendes Level
gap_atr REAL, -- Abstand Kurs<->Level in ATR_M15
p_break INTEGER, -- Durchbruchwahrscheinlichkeit in %
lesart TEXT, -- 'auf' | 'ab' | 'unklar' (die Richtungs-Lesart)
price REAL,
atr REAL, -- ATR_M15 zur Normierung
ret30 REAL, -- (Kurs[+30min]-price) x Lesart-Richtung / atr
ret60 REAL,
hit30 INTEGER, -- 1 = Kurs lief in Richtung der Lesart
hit60 INTEGER,
done INTEGER
)
""",
"""
CREATE TABLE IF NOT EXISTS verdict_votes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER, -- lokale Epoch
@@ -458,6 +486,63 @@ class HistoryLogger:
conn.commit()
return cur.lastrowid
def log_m15_state(self, *, status: str, veto, kosten, level, gap_atr,
p_break, lesart, price, atr) -> int:
"""Loggt EINEN Zustandswechsel der M15-Karte.
⚠ Nur bei WECHSEL aufrufen, nicht je Tick — sonst entsteht dieselbe
Cluster-Verzerrung, die am 07.08. eine Untersuchung mit elf widerlegten
Hypothesen ausgeloest hat (roh 922 Zeilen mit 52,6 %, entkoppelt 93 mit
45,2 %). Ausgewertet wird separat gegen `candles_m1`.
⚠ Fail-open wie der Rest dieser Klasse: ein Logging-Fehler darf den
Handelspfad nie stoeren."""
try:
with self._lock, self._connect() as conn:
cur = conn.execute("""
INSERT INTO m15_states
(ts,status,veto,kosten,level,gap_atr,p_break,lesart,price,atr)
VALUES (?,?,?,?,?,?,?,?,?,?)
""", (int(time.time()), status or "", veto,
None if kosten is None else float(kosten),
None if level is None else float(level),
None if gap_atr is None else float(gap_atr),
None if p_break is None else int(p_break),
lesart, float(price or 0.0), float(atr or 0.0)))
conn.commit()
return cur.lastrowid
except Exception:
return 0
def m15_accuracy(self, last_n: int = 200) -> dict:
"""Trefferquote der LESART (nicht des Status) ueber die letzten
ausgewerteten Zustaende mit gerichteter Lesart.
⚠ Gegen 50 % zu lesen, nicht gegen 0. Und ⚠ die Karte behauptet KEINE
freie Richtung — gemessen wird nur, ob die BEDINGTE Lesart am Level
(„Abprall -> eher abwaerts") getroffen hat."""
try:
with self._lock, self._connect() as conn:
rows = conn.execute("""
SELECT hit30, hit60, ret30 FROM m15_states
WHERE done=1 AND lesart IN ('auf','ab')
ORDER BY id DESC LIMIT ?
""", (int(last_n),)).fetchall()
offen = conn.execute(
"SELECT COUNT(*) FROM m15_states WHERE done IS NULL "
"AND lesart IN ('auf','ab')").fetchone()[0]
except Exception:
return {"n": 0}
n = len(rows)
if not n:
return {"n": 0, "offen": offen}
h30 = [r[0] for r in rows if r[0] is not None]
h60 = [r[1] for r in rows if r[1] is not None]
r30 = [r[2] for r in rows if r[2] is not None]
return {"n": n, "offen": offen,
"wr30": round(100 * sum(h30) / len(h30)) if h30 else None,
"wr60": round(100 * sum(h60) / len(h60)) if h60 else None,
"avg30": round(sum(r30) / len(r30), 3) if r30 else None}
def rec_accuracy(self, last_n: int = 200) -> dict:
"""Trefferquote der letzten `last_n` ausgewerteten Empfehlungs-Episoden.