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:
co-authored by
Claude Opus 5
parent
5ddbe07066
commit
86c432f791
+173
-2
@@ -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 (45–55 % = 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 45–55 %) 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,
|
||||
|
||||
Reference in New Issue
Block a user