Gesamtempfehlung-Paket: 2 Rollouts, 3 Anzeige-Features, 2 Messungen (v=114)
- Entry-Raum-Gate 0,6→1,0 (Messung lag vor: beide Hälften besser) - Konfidenz-Kalibrierung gemessen (analyze_verdict_calibration.py): conf INVERTIERT zwischen Regimen → keine P(Erfolg)-Aufwertung, kein Konf-Sizing - verdict_votes-Logging (1×/min) für spätere Copilot/Elliott-Entscheidung - Order-Dialog zeigt eigenen Ausrichtungs-Split (36/40 Trades ohne Signal: −423€) - Live-Kosten-Chip (Spread/ATR) im Verdict - P(break)×Entry-Raum-Freigabe gemessen VERWORFEN (Flip in jeder Schwelle, backtest_entryroom_pbreak.py) — 13. verworfener Signal-Eingriff - News-Konflikt-Chip + recommendations.news_score wieder befüllt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c3c6a8ce5f
commit
d61c4a3634
+5
-2
@@ -313,8 +313,11 @@ DEFAULT_CONFIG = {
|
||||
# Entry-Raum-Gate: Signal → WARTEN, wenn das Gegenlevel (M5-Pivot in Trade-
|
||||
# Richtung) näher als X×ATR liegt (gemessen `backtest_entryroom.py` — Raum
|
||||
# <0,6 in beiden Hälften negativ: Ertrag am Level gedeckelt, Kosten fressen
|
||||
# den Rest). 0 = aus.
|
||||
"entry_room_atr": "0.6",
|
||||
# den Rest). 0 = aus. **0,6→1,0 am 2026-07-24 (User: „baue alles ein"):**
|
||||
# 1,0 war von Anfang an in BEIDEN Hälften besser (H1 +415/H2 +266 R), Start
|
||||
# 0,6 war nur die konservative Stufe (1,0 filtert ~¾ der Signale → deutlich
|
||||
# mehr WARTEN-Phasen; gemessen trotzdem mehr Gesamtertrag).
|
||||
"entry_room_atr": "1.0",
|
||||
# Tageszeit-Gate: Komma-Liste der Stunden (Berlin), zu denen die Wellen-
|
||||
# Empfehlung WARTEN erzwingt. **Gemessener Default = 0–7,12,16** (Nacht-
|
||||
# Kostenfalle + 12/16 Uhr beidhälftig negativ, `backtest_hourly_split.py`).
|
||||
|
||||
+59
-1
@@ -947,6 +947,13 @@ class TradingEngine:
|
||||
# history.log_recommendation → flutet nicht).
|
||||
try:
|
||||
adv = (self.agent.snapshot() or {}).get("advisory") or {}
|
||||
# News-Sentiment mitloggen (2026-07-24, Idee 6): war seit einer Weile
|
||||
# None — wieder befüllt, damit die Prädiktivität in ein paar Monaten
|
||||
# messbar ist (analyze_verdict_calibration.py fand H2 nur n=4/6).
|
||||
try:
|
||||
_news = float((self.news.sentiment or {}).get("score") or 0.0)
|
||||
except Exception:
|
||||
_news = None
|
||||
self.history.log_recommendation(
|
||||
signal=rec.get("signal"), score=float(rec.get("score") or 0.0),
|
||||
conf_pct=int(rec.get("conf_pct") or 0),
|
||||
@@ -954,10 +961,31 @@ 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=None)
|
||||
news_score=_news)
|
||||
except Exception as e:
|
||||
log.warning(f"log_recommendation: {e}")
|
||||
|
||||
# Verdict-Stimmen-Logging (2026-07-24, Idee 2b): Modul-Votes 1×/min in die
|
||||
# DB (`verdict_votes`), damit die UNBELEGTEN Stimmen (KI-Copilot, Elliott)
|
||||
# nach ein paar Wochen datenbasiert bewertet werden können (behalten /
|
||||
# Gewicht senken / raus wie TU). Erste Messung (analyze_verdict_calibration.
|
||||
# py, 2026-07-24): Copilot nicht robust prädiktiv, n aber klein → sammeln.
|
||||
try:
|
||||
vd = self._verdict(s, rec, self.wave.snapshot(), self.tu.snapshot(),
|
||||
self.agent.snapshot(), self.elliott.snapshot())
|
||||
vv = {v["name"]: v for v in vd.get("votes") or []}
|
||||
def _v(name, key="vote"):
|
||||
return (vv.get(name) or {}).get(key, 0)
|
||||
self.history.log_verdict_votes(
|
||||
headline=vd.get("headline") or "WARTEN",
|
||||
conf=int(vd.get("conf") or 0), bias=float(vd.get("bias") or 0.0),
|
||||
tf=vd.get("tf") or "", wave=_v("Welle"), m30=_v("M30"), h1=_v("H1"),
|
||||
ki=_v("KI-Copilot"), ki_w=float(_v("KI-Copilot", "weight") or 0.0),
|
||||
elliott=_v("Elliott"), elliott_w=float(_v("Elliott", "weight") or 0.0),
|
||||
squeeze=_v("Squeeze"), news_score=_news)
|
||||
except Exception as e:
|
||||
log.debug(f"log_verdict_votes: {e}")
|
||||
|
||||
# Kontext für Trade-Logging (set_open_context beim Öffnen)
|
||||
self._last_rec = {
|
||||
"rec_signal": rec.get("signal"),
|
||||
@@ -1651,6 +1679,21 @@ class TradingEngine:
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
_SNAP_TTL_S = 0.8 # geteilter Snapshot gilt so lange als frisch
|
||||
|
||||
def _alignment_cached(self) -> dict | None:
|
||||
"""Ausrichtungs-Split (mit/gegen/ohne Empfehlung, letzte 40 Trades) für den
|
||||
Order-Bestätigungsdialog — ehrliche eigene Zahlen statt generischem Hinweis
|
||||
(2026-07-24, Verhaltens-Hebel: Gegen-Signal-Trades sind die gemessene
|
||||
Kern-Leckage). DB-Query gecacht (~120 s), Snapshot bleibt billig."""
|
||||
now = time.time()
|
||||
if now - getattr(self, "_align_ts", 0.0) > 120:
|
||||
try:
|
||||
self._align_cache = self.history.alignment_stats(40)
|
||||
except Exception as e:
|
||||
log.debug(f"alignment_stats: {e}")
|
||||
self._align_cache = None
|
||||
self._align_ts = now
|
||||
return getattr(self, "_align_cache", None)
|
||||
|
||||
def snapshot_cached(self) -> dict:
|
||||
"""Teilt EINEN frisch berechneten Snapshot über alle WS-Clients/HTTP-
|
||||
Aufrufe (TTL ~0,8 s). Spart SQLite/Verdict-Last bei mehreren Geräten —
|
||||
@@ -1903,6 +1946,21 @@ class TradingEngine:
|
||||
"all": self._compact_stats("all")},
|
||||
"close_alert_count": self._close_alert_count,
|
||||
"news_storm": self.news.storm_state(),
|
||||
# News-Sentiment-Score (−1..+1) für den Konflikt-Chip (2026-07-24):
|
||||
# steht der Headline-Flow stark gegen Signal/Position, zeigt das
|
||||
# Frontend eine Warnung (reiner Kontext — Prädiktivität unbelegt,
|
||||
# wird via recommendations.news_score gesammelt und später gemessen).
|
||||
"news_sentiment": (lambda sc: round(float(sc), 2) if sc is not None else None)(
|
||||
(self.news.sentiment or {}).get("score")),
|
||||
"alignment": self._alignment_cached(),
|
||||
# Live-Handelskosten relativ zur Vola (2026-07-24): Spread/ATR(M5).
|
||||
# Gemessen (`backtest_realcosts.py`): Ø 0,265×ATR, Nacht 0,32–0,50 =
|
||||
# Kostenfalle, 15–17 Uhr ~0,13 = günstig. Reine Anzeige (Chip) — die
|
||||
# informierende Alternative zum abgeschalteten Dead-Hours-Gate.
|
||||
"cost_ratio": (lambda b, a, atr: round((a - b) / max(atr, 0.06), 2)
|
||||
if (b and a and atr) else None)(
|
||||
(market or {}).get("bid"), (market or {}).get("ask"),
|
||||
((wave_snap or {}).get("pb_feats") or {}).get("atr")),
|
||||
"sr_close_hint": sr_close_hint,
|
||||
"stop_approach": stop_approach,
|
||||
"auto_sr_close": self._auto_sr_close,
|
||||
|
||||
@@ -138,6 +138,25 @@ SCHEMA = [
|
||||
correct INTEGER -- NULL bis ausgewertet, dann 0/1
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS verdict_votes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ts INTEGER, -- lokale Epoch
|
||||
headline TEXT, -- Wave-Signal (LONG/SHORT/WARTEN)
|
||||
conf INTEGER, -- conf_pct der Welle
|
||||
bias REAL, -- gewichteter Konsens-Bias −1..+1
|
||||
tf TEXT, -- Zeitebene der Empfehlung
|
||||
wave INTEGER, -- Votes je Modul: +1/−1/0
|
||||
m30 INTEGER,
|
||||
h1 INTEGER,
|
||||
ki INTEGER, -- KI-Copilot-Vote (0 auch wenn ohne Aussage)
|
||||
ki_w REAL, -- Copilot-Gewicht (0 = hatte keine Aussage)
|
||||
elliott INTEGER,
|
||||
elliott_w REAL,
|
||||
squeeze INTEGER,
|
||||
news_score REAL -- News-Sentiment zum Zeitpunkt (Kontext)
|
||||
)
|
||||
""",
|
||||
]
|
||||
|
||||
# Indizes — werden NACH den Migrationen ausgeführt, weil sie Spalten
|
||||
@@ -156,6 +175,7 @@ INDEXES = [
|
||||
"CREATE INDEX IF NOT EXISTS idx_intended_close ON intended_trades(close_ts)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_pbreak_ts ON pbreak_predictions(ts)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_pbreak_open ON pbreak_predictions(outcome) WHERE outcome IS NULL",
|
||||
"CREATE INDEX IF NOT EXISTS idx_verdict_ts ON verdict_votes(ts)",
|
||||
]
|
||||
|
||||
# Migrations: Spalten, die in alten DBs evtl. fehlen.
|
||||
@@ -394,6 +414,61 @@ class HistoryLogger:
|
||||
"pending": pending,
|
||||
}
|
||||
|
||||
def log_verdict_votes(self, *, headline: str, conf: int, bias: float, tf: str,
|
||||
wave: int, m30: int, h1: int, ki: int, ki_w: float,
|
||||
elliott: int, elliott_w: float, squeeze: int,
|
||||
news_score=None) -> None:
|
||||
"""Verdict-Modul-Stimmen für die spätere Kalibrier-Auswertung loggen
|
||||
(Copilot/Elliott sind unbelegte Stimmen — nach ein paar Wochen kann
|
||||
`analyze_verdict_calibration.py`-artig entschieden werden, ob sie
|
||||
Gewicht behalten). Intern gedrosselt auf max. 1×/60 s."""
|
||||
now = time.time()
|
||||
if now - getattr(self, "_verdict_log_ts", 0.0) < 60:
|
||||
return
|
||||
self._verdict_log_ts = now
|
||||
with self._lock, self._connect() as conn:
|
||||
conn.execute("""
|
||||
INSERT INTO verdict_votes
|
||||
(ts, headline, conf, bias, tf, wave, m30, h1, ki, ki_w,
|
||||
elliott, elliott_w, squeeze, news_score)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
""", (int(now), headline, conf, bias, tf, wave, m30, h1, ki, ki_w,
|
||||
elliott, elliott_w, squeeze, news_score))
|
||||
conn.commit()
|
||||
|
||||
def alignment_stats(self, last_n: int = 40) -> dict:
|
||||
"""Ausrichtungs-Split der letzten N geschlossenen Trades: liefen sie MIT,
|
||||
GEGEN oder OHNE die Empfehlung (rec_signal beim Entry)? Für die ehrliche
|
||||
Verhaltens-Anzeige im Order-Bestätigungsdialog (B0 als Software — die
|
||||
gemessene Kern-Leckage sind diskretionäre Gegen-Signal-Trades)."""
|
||||
with self._connect() as conn:
|
||||
rows = conn.execute("""
|
||||
SELECT direction, rec_signal, pnl FROM trades
|
||||
WHERE exit_time IS NOT NULL AND pnl IS NOT NULL
|
||||
ORDER BY exit_time DESC LIMIT ?
|
||||
""", (last_n,)).fetchall()
|
||||
out = {}
|
||||
for key, match in (("with", True), ("against", False)):
|
||||
grp = []
|
||||
for r in rows:
|
||||
rec = (r["rec_signal"] or "").upper()
|
||||
if rec not in ("LONG", "SHORT"):
|
||||
continue
|
||||
aligned = (rec == "LONG") == (r["direction"] == "BUY")
|
||||
if aligned == match:
|
||||
grp.append(r["pnl"])
|
||||
n = len(grp)
|
||||
out[key] = {"n": n,
|
||||
"wr": round(100 * sum(1 for p in grp if p > 0) / n) if n else None,
|
||||
"pnl": round(sum(grp), 2)}
|
||||
none_grp = [r["pnl"] for r in rows
|
||||
if (r["rec_signal"] or "").upper() not in ("LONG", "SHORT")]
|
||||
n = len(none_grp)
|
||||
out["none"] = {"n": n,
|
||||
"wr": round(100 * sum(1 for p in none_grp if p > 0) / n) if n else None,
|
||||
"pnl": round(sum(none_grp), 2)}
|
||||
return out
|
||||
|
||||
# ══════════════════════════════════════════
|
||||
# STATISTIK-QUERIES
|
||||
# ══════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user