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
@@ -222,8 +222,9 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
|
|||||||
= späterer Einstieg, Anfang des Moves verschenkt). ⚠ Ein-Stichproben-Lauf hatte
|
= späterer Einstieg, Anfang des Moves verschenkt). ⚠ Ein-Stichproben-Lauf hatte
|
||||||
„1,0 besser" gezeigt — Split-Prüfung drehte das (B2!). Nebeneffekt: kürzere
|
„1,0 besser" gezeigt — Split-Prüfung drehte das (B2!). Nebeneffekt: kürzere
|
||||||
„warte auf Breakout"-Phasen.
|
„warte auf Breakout"-Phasen.
|
||||||
- **Entry-Raum-Gate (AKTIV, `[trading] entry_room_atr=0.6`, gemessen
|
- **Entry-Raum-Gate (AKTIV, `[trading] entry_room_atr=1.0` — 0,6→1,0 am
|
||||||
`backtest_entryroom.py` 2026-07-16):** Richtungssignal → WARTEN, wenn das
|
2026-07-24, gemessen `backtest_entryroom.py` 2026-07-16):** Richtungssignal →
|
||||||
|
WARTEN, wenn das
|
||||||
**Gegenlevel** (M5-Pivot in Trade-Richtung, trainingsgleich zum S/R-Auto-Close)
|
**Gegenlevel** (M5-Pivot in Trade-Richtung, trainingsgleich zum S/R-Auto-Close)
|
||||||
näher als X×ATR_M5 liegt. Befund (seq. Sim MIT S/R-Close-Exit, Echtkosten,
|
näher als X×ATR_M5 liegt. Befund (seq. Sim MIT S/R-Close-Exit, Echtkosten,
|
||||||
2 Halbjahre): **monoton in BEIDEN Hälften** — Raum <0,3: ØR −0,119/−0,067
|
2 Halbjahre): **monoton in BEIDEN Hälften** — Raum <0,3: ØR −0,119/−0,067
|
||||||
@@ -235,9 +236,19 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
|
|||||||
4 der 5 sr_closes desselben Tages waren dagegen RETTER — Exit NICHT anfassen).
|
4 der 5 sr_closes desselben Tages waren dagegen RETTER — Exit NICHT anfassen).
|
||||||
Umsetzung: `wave._room_gate` (in `refresh_market` VOR `_confirm_breakout`;
|
Umsetzung: `wave._room_gate` (in `refresh_market` VOR `_confirm_breakout`;
|
||||||
`set_entry_room`, Grund „kein Raum: Widerstand X nur 0,4×ATR entfernt …").
|
`set_entry_room`, Grund „kein Raum: Widerstand X nur 0,4×ATR entfernt …").
|
||||||
Backtests via `_build` bleiben gate-frei (kein pb_levels-State). 1,0 wäre gemessen
|
Backtests via `_build` bleiben gate-frei (kein pb_levels-State). **0,6→1,0
|
||||||
noch besser (H1 +415/H2 +266 R), filtert aber ~¾ der Signale → Start 0,6, per
|
ausgerollt (2026-07-24, „baue alles ein"):** 1,0 war von Anfang an in BEIDEN
|
||||||
B4-Wochenreport prüfen und ggf. via ini erhöhen.
|
Hälften besser (H1 +415/H2 +266 R); der konservative 0,6-Start diente nur der
|
||||||
|
Frequenz (~¾ der Signale gefiltert → deutlich mehr WARTEN). Live-Kurzcheck vor
|
||||||
|
dem Rollout: S/R-Closes seit 16.07. +925 € bei nur noch 28 % Klein-Closes (<5 €)
|
||||||
|
— Gate wirkt wie gemessen. ⚠ **P(break)-Freigabe des Gates = VERWORFEN (gemessen
|
||||||
|
`backtest_entryroom_pbreak.py`, 2026-07-24):** Idee war, Raum<1,0-Signale
|
||||||
|
durchzulassen, wenn P(break)@Entry hoch (Level bricht eh → Ertrag nicht gedeckelt).
|
||||||
|
Das Modell TRENNT zwar auch am Entry (Freigabe-Gruppen klar besser als der Rest),
|
||||||
|
aber JEDE Schwelle kippt zwischen den Hälften (p≥0,5: −0,016/+0,083 · p≥0,6:
|
||||||
|
+0,089/−0,006 · p≥0,7: +0,076/−0,029) → nicht robust, reines Distanz-Gate bleibt.
|
||||||
|
**13. verworfener Signal-Eingriff.** (Nebenbefund: geblockte Gruppe in beiden
|
||||||
|
Hälften klar negativ, −436/−251 ΣR — bestätigt die 1,0-Erhöhung.)
|
||||||
- **Mindest-Konfidenz-Gate** (`_MIN_CONF=55`): Signal < 55 % → WARTEN. Gemessen
|
- **Mindest-Konfidenz-Gate** (`_MIN_CONF=55`): Signal < 55 % → WARTEN. Gemessen
|
||||||
(`backtest_conf.py`): Band **40–54 % = negativer Edge** (−0,025), ≥55 % = +0,058;
|
(`backtest_conf.py`): Band **40–54 % = negativer Edge** (−0,025), ≥55 % = +0,058;
|
||||||
Gate hebt Ø-Edge ~+50 % und den Gesamtertrag (entfernt die schwachen Setups, bei
|
Gate hebt Ø-Edge ~+50 % und den Gesamtertrag (entfernt die schwachen Setups, bei
|
||||||
@@ -983,6 +994,41 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
|
|||||||
z. B. „▲ LONG · M5" — TF, für die die Empfehlung gilt); Rest = nur Konsens-Anzeige.
|
z. B. „▲ LONG · M5" — TF, für die die Empfehlung gilt); Rest = nur Konsens-Anzeige.
|
||||||
Backend `engine._verdict` → `verdict` im Snapshot; Render `renderVerdict`
|
Backend `engine._verdict` → `verdict` im Snapshot; Render `renderVerdict`
|
||||||
(`app.js`). Details: `docs/gesamtempfehlung.md`.
|
(`app.js`). Details: `docs/gesamtempfehlung.md`.
|
||||||
|
**⚠ Konfidenz ist NICHT kalibriert (gemessen `analyze_verdict_calibration.py`,
|
||||||
|
2026-07-24, „Gesamtempfehlung verbessern"-Paket):** conf_pct gegen Forward-Return
|
||||||
|
(2 h, ×ATR, 1604 gesampelte Signale aus `recommendations`, 2 Hälften) — H1 (Mai–Jun)
|
||||||
|
perfekt monoton (conf 85–100: WR 68 %/ØR +1,13; <55 klar negativ), **H2 (Jun–Jul)
|
||||||
|
komplett INVERTIERT** (conf 85–100: WR 40 %/ØR −0,90; mittlere Bins besser). Hohe
|
||||||
|
Konf = starker Trend + Konfluenz — im Chop-/Hochvola-Regime sind genau das die
|
||||||
|
Überdehnungen vor der Wende. **Konsequenzen:** conf-Anzeige NICHT als P(Erfolg)
|
||||||
|
aufwerten; **Konfidenz-Sizing-Idee VERWORFEN** (hätte in H2 die schlechtesten
|
||||||
|
Signale am größten gesized); Ring bleibt, was er ist (Score-Aggregat). Copilot-Bias
|
||||||
|
in derselben Messung nicht robust prädiktiv (n klein bzw. ØR ~0) — für die
|
||||||
|
datenbasierte Entscheidung (behalten/Gewicht/raus wie TU) loggt jetzt
|
||||||
|
**`verdict_votes`** (neue DB-Tabelle, `history.log_verdict_votes`, 1×/60 s aus
|
||||||
|
`_run_analysis`: headline/conf/bias/tf + Modul-Votes Welle/M30/H1/KI(+w)/
|
||||||
|
Elliott(+w)/Squeeze + news_score) — Auswertung nach ein paar Wochen analog
|
||||||
|
`analyze_verdict_calibration.py`.
|
||||||
|
**Live-Kosten-Chip (2026-07-24):** Snapshot `cost_ratio` = (ask−bid)/ATR_M5
|
||||||
|
(`pb_feats.atr`, Floor 0,06) → in der `vd-consensus`-Zeile „Kosten 0,14×ATR ✅
|
||||||
|
günstig / · normal / ⚠ teuer" (Schwellen 0,20/0,32 aus `backtest_realcosts.py`:
|
||||||
|
Ø 0,265, Nacht 0,32–0,50 = Falle, 15–17 Uhr ~0,13). Reine Info — die ehrliche
|
||||||
|
Alternative zum abgeschalteten Dead-Hours-Gate.
|
||||||
|
**Ausrichtungs-Split im Order-Dialog (2026-07-24, Verhaltens-Hebel/B0):** Der
|
||||||
|
„trotzdem eröffnen?"-Dialog zeigt jetzt die EIGENEN Zahlen aus der DB
|
||||||
|
(`history.alignment_stats(40)` → Snapshot `alignment`, Engine-Cache ~120 s):
|
||||||
|
„📊 Deine Trades OHNE Signal: 42 % Trefferquote · −423 € (n36) / Mit Signal: …"
|
||||||
|
(Gruppen <5 Trades werden ausgeblendet). Real bei Einbau: 36 der letzten 40
|
||||||
|
Trades liefen OHNE Signal-Deckung mit −423 € — exakt die dokumentierte
|
||||||
|
Kern-Leckage, jetzt im Moment der Entscheidung sichtbar.
|
||||||
|
**News-Konflikt-Chip (2026-07-24, Auslöser 23.07.: zwei Shorts gegen einen
|
||||||
|
+0,98-Sentiment-Burst):** Snapshot `news_sentiment` (Score −1..+1 aus
|
||||||
|
`news.sentiment`); Frontend `#news-conflict` (Meldungen-Karte, bernstein) warnt
|
||||||
|
bei |Score|≥0,5 gegen offene Position bzw. aktives Signal („📰 News-Flow stark
|
||||||
|
bullisch (+0,98) — steht gegen deine SHORT-Position"). Reiner Kontext —
|
||||||
|
Prädiktivität unbelegt (Kalibrier-Messung: H2 nur n=4/6); dafür wird
|
||||||
|
`recommendations.news_score` seit 2026-07-24 wieder BEFÜLLT (war auf None
|
||||||
|
degradiert) → in ein paar Monaten messbar.
|
||||||
- **Karten** (Dashboard, von oben): Gesamtempfehlung · **Meldungen** · Marktstruktur ·
|
- **Karten** (Dashboard, von oben): Gesamtempfehlung · **Meldungen** · Marktstruktur ·
|
||||||
KI-Copilot · Letzte Trades · Kurslücken · **Statistik (live) ganz unten**.
|
KI-Copilot · Letzte Trades · Kurslücken · **Statistik (live) ganz unten**.
|
||||||
(Tagesziele-Kachel **entfernt** 2026-07-13. **Welle-Karte komplett entfernt
|
(Tagesziele-Kachel **entfernt** 2026-07-13. **Welle-Karte komplett entfernt
|
||||||
@@ -1140,7 +1186,7 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
|
|||||||
Signal → **Hinweis-Dialog „trotzdem eröffnen?"** (überschreibbar). CLOSE bei
|
Signal → **Hinweis-Dialog „trotzdem eröffnen?"** (überschreibbar). CLOSE bei
|
||||||
**negativem** P&L → Sicherheitsabfrage; im Plus/Breakeven direkt. Sonst keine
|
**negativem** P&L → Sicherheitsabfrage; im Plus/Breakeven direkt. Sonst keine
|
||||||
Token-Dialoge (`require_token=false`). Audio braucht 1× Nutzer-Tap.
|
Token-Dialoge (`require_token=false`). Audio braucht 1× Nutzer-Tap.
|
||||||
- Asset-Version aktuell **v=113** (in `web/index.html` hochzählen, siehe Workflows).
|
- Asset-Version aktuell **v=114** (in `web/index.html` hochzählen, siehe Workflows).
|
||||||
Schriftgrößen 2026-07-24 global **+4px** (2× je +2px auf User-Wunsch; Body-Basis
|
Schriftgrößen 2026-07-24 global **+4px** (2× je +2px auf User-Wunsch; Body-Basis
|
||||||
14→18px). ⚠ Betrifft in `style.css` sowohl `font-size:Npx` (71×) ALS AUCH die
|
14→18px). ⚠ Betrifft in `style.css` sowohl `font-size:Npx` (71×) ALS AUCH die
|
||||||
`font:<weight> Npx/…`-**Shorthand** (3×: `.ms-chip`/`.ms-bos-lbl`/`.sqm-badge` —
|
`font:<weight> Npx/…`-**Shorthand** (3×: `.ms-chip`/`.ms-bos-lbl`/`.sqm-badge` —
|
||||||
@@ -1227,7 +1273,7 @@ Aktivierung genullt (kein Altwert-Fehlalarm); Erkennung erst ab dem 2. Modify. T
|
|||||||
(Beobachtungen/Feedback) ergänzt diese Datei.
|
(Beobachtungen/Feedback) ergänzt diese Datei.
|
||||||
- `oil_widget_config.ini` enthält **Live-Secrets** (Keys/Token) — nicht ins
|
- `oil_widget_config.ini` enthält **Live-Secrets** (Keys/Token) — nicht ins
|
||||||
Repo/Logs leaken.
|
Repo/Logs leaken.
|
||||||
- **DB** `oil_widget_history.db` (trades · intended_trades · recommendations · **candles_m1**).
|
- **DB** `oil_widget_history.db` (trades · intended_trades · recommendations · **candles_m1** · **pbreak_predictions** · **verdict_votes**).
|
||||||
Vor jedem Eingriff Backup (`*.bak-<datum>`); Zeitspalten sind lokale Epoch
|
Vor jedem Eingriff Backup (`*.bak-<datum>`); Zeitspalten sind lokale Epoch
|
||||||
(Broker-Offset bereits korrigiert) — **AUSNAHME `candles_m1.time` = ROHE Broker-Zeit
|
(Broker-Offset bereits korrigiert) — **AUSNAHME `candles_m1.time` = ROHE Broker-Zeit
|
||||||
(UTC+3)**, deckungsgleich mit `copy_rates`.
|
(UTC+3)**, deckungsgleich mit `copy_rates`.
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Kalibrierung der Gesamtempfehlung (2026-07-24, Idee 2 „Verdict ans eigene
|
||||||
|
Erfolgsrezept halten"): Ist die angezeigte Konfidenz (conf_pct) PRÄDIKTIV — sagt
|
||||||
|
conf=80 mehr Erfolg voraus als conf=55? Und: sind die unbelegten Verdict-Stimmen
|
||||||
|
(KI-Copilot ai_sentiment, news_score) prädiktiv?
|
||||||
|
|
||||||
|
Methodik wie beim P(break)-Modell (dem kalibrierten Erfolgsfall):
|
||||||
|
- `recommendations` (LONG/SHORT) aus der DB, Sampling ≥15 min Abstand
|
||||||
|
(Autokorrelation dämpfen — die Empfehlung ändert sich minütlich kaum).
|
||||||
|
- Forward-Return = Signalrichtung × (Close[t+H] − Close[t0]) / ATR(M5, t0)
|
||||||
|
für Horizonte 1 h und 2 h (M5-Bars von MT5; rec.timestamp = lokale Epoch,
|
||||||
|
Bar-Zeit = Broker-Zeit → −3 h).
|
||||||
|
- Bins über conf_pct → n / WR(fwd>0) / Ø-fwdR je Bin, in ZWEI Zeitraum-Hälften
|
||||||
|
(Sign-Stabilität wie immer).
|
||||||
|
- Copilot: ai_sentiment (LONG/bullish=+1, SHORT/bearish=−1) → Ø-fwdR in
|
||||||
|
Copilot-Richtung (unabhängig vom Wave-Signal!). News: Vorzeichen news_score.
|
||||||
|
Kalibriert = monoton steigende Ø-fwdR über die conf-Bins, in BEIDEN Hälften.
|
||||||
|
"""
|
||||||
|
import sqlite3, datetime as dt
|
||||||
|
import MetaTrader5 as mt5
|
||||||
|
|
||||||
|
_BROKER_OFF = 3*3600
|
||||||
|
_SAMPLE_S = 15*60 # Mindestabstand zwischen zwei gewerteten Empfehlungen
|
||||||
|
_H1, _H2 = 12, 24 # Forward-Horizonte in M5-Bars (1 h / 2 h)
|
||||||
|
_ATR_P = 14
|
||||||
|
_BINS = [(0,45),(45,55),(55,65),(65,75),(75,85),(85,101)]
|
||||||
|
|
||||||
|
|
||||||
|
def _atr_series(H, L, C, p=_ATR_P):
|
||||||
|
t=[0.0]
|
||||||
|
for i in range(1,len(C)): t.append(max(H[i]-L[i],abs(H[i]-C[i-1]),abs(L[i]-C[i-1])))
|
||||||
|
return [(sum(t[max(1,i-p+1):i+1])/max(1,len(t[max(1,i-p+1):i+1]))) if i else None for i in range(len(C))]
|
||||||
|
|
||||||
|
|
||||||
|
def _dirnum(s):
|
||||||
|
if not s: return 0
|
||||||
|
s = str(s).strip().upper()
|
||||||
|
if s in ("LONG","BULLISH"): return 1
|
||||||
|
if s in ("SHORT","BEARISH"): return -1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def line(lbl, rows, key):
|
||||||
|
v=[r[key] for r in rows]
|
||||||
|
if not v: return f" {lbl:<22} —"
|
||||||
|
n=len(v); wr=100*sum(1 for x in v if x>0)/n
|
||||||
|
return f" {lbl:<22} n={n:>5} WR={wr:>3.0f}% ØR={sum(v)/n:+.3f}"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
mt5.initialize()
|
||||||
|
sym = next((c for c in ("SpotCrude","USOIL","WTI","XTIUSD") if mt5.symbol_info(c)), None)
|
||||||
|
bars = None
|
||||||
|
for req in (100000, 80000, 60000, 40000):
|
||||||
|
bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, req)
|
||||||
|
if bars is not None and len(bars) > 2000: break
|
||||||
|
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]
|
||||||
|
T=[int(b["time"])-_BROKER_OFF for b in bars] # → echte UTC-Epoch
|
||||||
|
A=_atr_series(H,L,C)
|
||||||
|
t2i = {t:i for i,t in enumerate(T)}
|
||||||
|
|
||||||
|
con = sqlite3.connect("oil_widget_history.db"); con.row_factory=sqlite3.Row
|
||||||
|
recs = con.execute(
|
||||||
|
"SELECT timestamp, signal, conf_pct, ai_sentiment, news_score "
|
||||||
|
"FROM recommendations WHERE signal IN ('LONG','SHORT') ORDER BY timestamp"
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
ev=[]; last_ts=0
|
||||||
|
for r in recs:
|
||||||
|
ts=r["timestamp"]
|
||||||
|
if ts-last_ts < _SAMPLE_S: continue
|
||||||
|
bar_t = (ts//300)*300 # auf M5-Raster runden
|
||||||
|
i = t2i.get(bar_t)
|
||||||
|
if i is None or i+_H2 >= len(C) or not A[i]: continue
|
||||||
|
last_ts=ts
|
||||||
|
d = 1 if r["signal"]=="LONG" else -1
|
||||||
|
atr=max(A[i],0.06)
|
||||||
|
ev.append(dict(ts=ts, conf=r["conf_pct"] or 0, d=d,
|
||||||
|
ai=_dirnum(r["ai_sentiment"]),
|
||||||
|
news=(r["news_score"] if r["news_score"] is not None else None),
|
||||||
|
f1=(C[i+_H1]-C[i])*d/atr, f2=(C[i+_H2]-C[i])*d/atr))
|
||||||
|
if not ev:
|
||||||
|
print("Keine matchbaren Empfehlungen (History-Überlappung prüfen)."); return
|
||||||
|
mid_ts = ev[len(ev)//2]["ts"]
|
||||||
|
halves=[("H1 (alt)",[e for e in ev if e["ts"]<mid_ts]),
|
||||||
|
("H2 (neu)",[e for e in ev if e["ts"]>=mid_ts])]
|
||||||
|
def span(rows):
|
||||||
|
return (f"{dt.datetime.fromtimestamp(rows[0]['ts']):%d.%m.%y}–"
|
||||||
|
f"{dt.datetime.fromtimestamp(rows[-1]['ts']):%d.%m.%y}")
|
||||||
|
print("="*84)
|
||||||
|
print(f" Verdict-Kalibrierung — {sym}, {len(ev)} gesampelte Signale (≥15 min Abstand)")
|
||||||
|
print(f" fwd-Return in Signalrichtung, ×ATR(M5) · Horizonte 1 h/2 h")
|
||||||
|
print("="*84)
|
||||||
|
|
||||||
|
for lbl, rows in halves:
|
||||||
|
print(f"\n{lbl} ({span(rows)}, n={len(rows)}):")
|
||||||
|
print(" conf_pct-Bins (Horizont 2 h) — kalibriert = ØR steigt monoton:")
|
||||||
|
for lo,hi in _BINS:
|
||||||
|
sub=[e for e in rows if lo<=e["conf"]<hi]
|
||||||
|
print(line(f"conf {lo}–{hi-1}", sub, "f2"))
|
||||||
|
print(" KI-Copilot (eigene Richtung, unabhängig vom Wave-Signal):")
|
||||||
|
for tag,dd in (("Copilot LONG",1),("Copilot SHORT",-1)):
|
||||||
|
sub=[dict(f2=e["f2"]*e["d"]*dd) for e in rows if e["ai"]==dd]
|
||||||
|
print(line(tag, sub, "f2"))
|
||||||
|
print(" News-Score (Richtung des Scores, |score|≥0.3):")
|
||||||
|
for tag,dd in (("News bullisch",1),("News bärisch",-1)):
|
||||||
|
sub=[dict(f2=e["f2"]*e["d"]*dd) for e in rows
|
||||||
|
if e["news"] is not None and (e["news"]>=0.3 if dd>0 else e["news"]<=-0.3)]
|
||||||
|
print(line(tag, sub, "f2"))
|
||||||
|
|
||||||
|
print("\n Lesart: conf kalibriert → höhere Bins klar bessere ØR in BEIDEN Hälften.")
|
||||||
|
print(" Copilot/News prädiktiv → 'LONG'-Zeile positiv UND 'SHORT'-Zeile positiv")
|
||||||
|
print(" (jeweils fwd in der EIGENEN Richtung gemessen).")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""P(break) × Entry-Raum (2026-07-24, Idee 5 aus „Gesamtempfehlung verbessern"):
|
||||||
|
Das Entry-Raum-Gate blockt Signale, deren GEGENLEVEL näher als X×ATR liegt —
|
||||||
|
Begründung: Ertrag dort gedeckelt (63 % Containment) − Kosten < 0. ABER: Wenn das
|
||||||
|
kalibrierte P(break)-Modell schon am ENTRY sagt, das Level bricht wahrscheinlich
|
||||||
|
(hoher Anlauf-Schwung), ist der Ertrag NICHT gedeckelt → das Gate blockt dann
|
||||||
|
möglicherweise zu Unrecht.
|
||||||
|
|
||||||
|
Test: gleiche Sim wie backtest_entryroom.py (seq., Live-Exit inkl. S/R-Close,
|
||||||
|
Echtkosten). Für Trades mit Raum < 1,0×ATR (die das Gate ab heute blockt) wird
|
||||||
|
P(break) AM ENTRY berechnet (mom6/mom3/wt/dist zum Gegenlevel). Politik-Vergleich:
|
||||||
|
GATE = alle Raum<1,0 blocken (Live-Politik seit 2026-07-24)
|
||||||
|
FREI p≥X = Raum<1,0 nur blocken, wenn P(break)@Entry < X (X=0,5/0,6/0,7)
|
||||||
|
Einbau NUR, wenn die freigegebene Gruppe (p≥X) in BEIDEN Hälften klar positiv ist
|
||||||
|
(sonst bleibt das reine Distanz-Gate). ⚠ Caveat: Modell wurde auf Touch-Momente
|
||||||
|
trainiert, nicht auf Entry-Kontexte — der Test misst deshalb direkt den Ertrag.
|
||||||
|
"""
|
||||||
|
import sys, bisect
|
||||||
|
import MetaTrader5 as mt5
|
||||||
|
from core.analysis import calc_trend_angle
|
||||||
|
from core.engine import _p_break
|
||||||
|
from core.wave_rec import (WaveRecommender, _atr, _ema_last, _ema_series,
|
||||||
|
_EMA_FAST, _EMA_SLOW, _N_BARS, _HTF_DEADBAND, _ANGLE_LR)
|
||||||
|
_MAXH = 288; _ATRMIN = 0.12; _PIV_K = 3; _LOOKBACK = 300; _PTHR = 0.60
|
||||||
|
_ROOM = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def _atr_series(H, L, C, p=14):
|
||||||
|
t = [0.0]
|
||||||
|
for i in range(1, len(C)):
|
||||||
|
t.append(max(H[i]-L[i], abs(H[i]-C[i-1]), abs(L[i]-C[i-1])))
|
||||||
|
return [(sum(t[max(1, i-p+1):i+1])/max(1, len(t[max(1, i-p+1):i+1]))) if i else None
|
||||||
|
for i in range(len(C))]
|
||||||
|
|
||||||
|
|
||||||
|
def st(Rs):
|
||||||
|
if not Rs: return None
|
||||||
|
n = len(Rs); w = sum(1 for x in Rs if x > 0); s = sum(Rs)
|
||||||
|
up = sum(x for x in Rs if x > 0); dn = -sum(x for x in Rs if x < 0)
|
||||||
|
return dict(n=n, wr=100*w/n, oR=s/n, pf=(up/dn if dn > 0 else 9.99), sum=s)
|
||||||
|
|
||||||
|
|
||||||
|
def line(lbl, s):
|
||||||
|
if not s: return f" {lbl:<30} —"
|
||||||
|
return (f" {lbl:<30} n={s['n']:>4} WR={s['wr']:>3.0f}% ØR={s['oR']:+.3f} "
|
||||||
|
f"PF={s['pf']:>4.2f} ΣR={s['sum']:>+6.0f}")
|
||||||
|
|
||||||
|
|
||||||
|
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+_N_BARS+_MAXH+5)
|
||||||
|
m30 = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M30, 0, n//6+500)
|
||||||
|
h1 = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_H1, 0, n//12+500)
|
||||||
|
si = mt5.symbol_info(sym); point = si.point
|
||||||
|
mt5.shutdown()
|
||||||
|
T = [int(b["time"]) for b in bars]; H = [float(b["high"]) for b in bars]
|
||||||
|
L = [float(b["low"]) for b in bars]; C = [float(b["close"]) for b in bars]
|
||||||
|
SP = [float(b["spread"])*point for b in bars]
|
||||||
|
A = _atr_series(H, L, C)
|
||||||
|
|
||||||
|
def series(rr):
|
||||||
|
t = [int(b["time"]) for b in rr]; c = [float(b["close"]) for b in rr]
|
||||||
|
hh = [float(b["high"]) for b in rr]; ll = [float(b["low"]) for b in rr]
|
||||||
|
return t, _ema_series(c, _EMA_FAST), _ema_series(c, _EMA_SLOW), _atr_series(hh, ll, c)
|
||||||
|
mT, mEf, mEs, mA = series(m30)
|
||||||
|
hT, hEf, hEs, hA = series(h1)
|
||||||
|
|
||||||
|
def tf_sign(tt, ef, es, aa, ts):
|
||||||
|
k = bisect.bisect_right(tt, ts)-1
|
||||||
|
if k < _EMA_SLOW or aa[k] is None or aa[k] <= 0: return 0
|
||||||
|
dd = ef[k]-es[k]
|
||||||
|
return 0 if abs(dd) < _HTF_DEADBAND*aa[k] else (1 if dd > 0 else -1)
|
||||||
|
|
||||||
|
w = WaveRecommender(type("T", (), {"snapshot": lambda s: {"intervals": {}}})(), mt5.TIMEFRAME_M5)
|
||||||
|
EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW)
|
||||||
|
sig = [0]*len(C)
|
||||||
|
for i in range(_N_BARS, len(C)-1):
|
||||||
|
wc = C[i-_N_BARS:i]; wh = H[i-_N_BARS:i]; wl = L[i-_N_BARS:i]
|
||||||
|
atr = _atr(wh, wl, wc)
|
||||||
|
if not atr or atr <= 0: continue
|
||||||
|
ef = _ema_last(wc, _EMA_FAST); es = _ema_last(wc, _EMA_SLOW)
|
||||||
|
a5 = calc_trend_angle(C[i-_ANGLE_LR-2:i], _ANGLE_LR)
|
||||||
|
rec, _ = w._build(ef, es, C[i-1], atr, "M5", 0,
|
||||||
|
htf_trend=tf_sign(mT, mEf, mEs, mA, T[i]),
|
||||||
|
h1_trend=tf_sign(hT, hEf, hEs, hA, T[i]), angle=a5)
|
||||||
|
s_ = rec["signal"]
|
||||||
|
sig[i] = 1 if s_ == "LONG" else -1 if s_ == "SHORT" else 0
|
||||||
|
|
||||||
|
phE = [(p+_PIV_K, H[p]) for p in range(_PIV_K, len(C)-_PIV_K)
|
||||||
|
if H[p] == max(H[p-_PIV_K:p+_PIV_K+1])]
|
||||||
|
plE = [(p+_PIV_K, L[p]) for p in range(_PIV_K, len(C)-_PIV_K)
|
||||||
|
if L[p] == min(L[p-_PIV_K:p+_PIV_K+1])]
|
||||||
|
phT = [e[0] for e in phE]; plT = [e[0] for e in plE]
|
||||||
|
|
||||||
|
def next_level(i, px, d):
|
||||||
|
if d > 0:
|
||||||
|
k = bisect.bisect_right(phT, i)
|
||||||
|
cands = [pr for (t_, pr) in phE[max(0, k-80):k] if t_ >= i-_LOOKBACK and pr > px]
|
||||||
|
return min(cands) if cands else None
|
||||||
|
k = bisect.bisect_right(plT, i)
|
||||||
|
cands = [pr for (t_, pr) in plE[max(0, k-80):k] if t_ >= i-_LOOKBACK and pr < px]
|
||||||
|
return max(cands) if cands else None
|
||||||
|
|
||||||
|
def cost(i, atr): return (SP[i] if SP[i] > 0 else 0.0225)/atr
|
||||||
|
|
||||||
|
def run(lo, hi_):
|
||||||
|
"""→ Liste (dist0, p_entry, R): Entry-Raum, P(break)@Entry, Netto-R."""
|
||||||
|
out = []
|
||||||
|
i = lo
|
||||||
|
while i < hi_:
|
||||||
|
d = sig[i]
|
||||||
|
if d == 0:
|
||||||
|
i += 1; continue
|
||||||
|
atr = max(A[i] or 0, _ATRMIN)
|
||||||
|
entry = C[i]
|
||||||
|
lvl0 = next_level(i, entry, d)
|
||||||
|
dist0 = (lvl0-entry)*d/atr if lvl0 else None
|
||||||
|
p_entry = None
|
||||||
|
if lvl0 is not None and i >= 7:
|
||||||
|
mom6 = (C[i]-C[i-6])/atr*d; mom3 = (C[i]-C[i-3])/atr*d
|
||||||
|
wt = 1.0 if (EF[i]-ES[i])*d > 0 else 0.0
|
||||||
|
p_entry = _p_break(mom6, mom3, wt, abs(lvl0-entry)/atr)
|
||||||
|
lvl = lvl0
|
||||||
|
eff = entry - d*2.0*atr; hw = entry
|
||||||
|
end = min(i+_MAXH, len(C)-1); exit_px = C[end]; exit_j = end
|
||||||
|
for j in range(i+1, end+1):
|
||||||
|
hj, lj = H[j], L[j]
|
||||||
|
if (lj <= eff) if d > 0 else (hj >= eff):
|
||||||
|
exit_px = eff; exit_j = j; break
|
||||||
|
hw = max(hw, hj) if d > 0 else min(hw, lj)
|
||||||
|
prof = (C[j]-entry)*d
|
||||||
|
if lvl is not None and ((hj >= lvl) if d > 0 else (lj <= lvl)):
|
||||||
|
a_j = max(A[j] or atr, _ATRMIN)
|
||||||
|
if j >= 7:
|
||||||
|
mom6 = (C[j]-C[j-6])/a_j*d; mom3 = (C[j]-C[j-3])/a_j*d
|
||||||
|
wt = 1.0 if (EF[j]-ES[j])*d > 0 else 0.0
|
||||||
|
p = _p_break(mom6, mom3, wt, abs(lvl-entry)/a_j)
|
||||||
|
if p < _PTHR and (lvl-entry)*d > 0:
|
||||||
|
exit_px = lvl; exit_j = j; break
|
||||||
|
lvl = next_level(j, C[j], d)
|
||||||
|
if prof >= 0.3*atr:
|
||||||
|
cand = hw - d*1.5*atr
|
||||||
|
if prof >= 1.3*atr:
|
||||||
|
cand = max(cand, entry) if d > 0 else min(cand, entry)
|
||||||
|
eff = max(eff, cand) if d > 0 else min(eff, cand)
|
||||||
|
out.append((dist0, p_entry, (exit_px-entry)*d/atr - cost(i, atr)))
|
||||||
|
i = exit_j + 1
|
||||||
|
return out
|
||||||
|
|
||||||
|
mid = len(C)//2
|
||||||
|
print("="*92)
|
||||||
|
print(f" P(break)×Entry-Raum — {sym} M5 (seq. Sim, Live-Exit inkl. S/R-Close, Echtkosten)")
|
||||||
|
print(f" Frage: dürfen Raum<{_ROOM}×ATR-Signale durch, wenn P(break)@Entry hoch ist?")
|
||||||
|
print("="*92)
|
||||||
|
for lbl, lo, hi_ in (("H1 (alt)", _N_BARS, mid), ("H2 (neu)", mid, len(C)-_MAXH-1)):
|
||||||
|
res = run(lo, hi_)
|
||||||
|
blocked = [(p, r) for dd, p, r in res if dd is not None and dd < _ROOM]
|
||||||
|
print(f"\n{lbl}: ({len(res)} Trades gesamt · {len(blocked)} im Raum<{_ROOM} = vom Gate geblockt)")
|
||||||
|
print(line("geblockte Gruppe GESAMT", st([r for _, r in blocked])))
|
||||||
|
for X in (0.5, 0.6, 0.7):
|
||||||
|
freed = [r for p, r in blocked if p is not None and p >= X]
|
||||||
|
rest = [r for p, r in blocked if p is None or p < X]
|
||||||
|
print(line(f" FREI wenn p≥{X:.1f}", st(freed)))
|
||||||
|
print(line(f" bleibt geblockt (p<{X:.1f})", st(rest)))
|
||||||
|
print("\n Einbau NUR, wenn 'FREI p≥X' in BEIDEN Hälften klar positiv (ØR netto ≳ +0,1).")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+5
-2
@@ -313,8 +313,11 @@ DEFAULT_CONFIG = {
|
|||||||
# Entry-Raum-Gate: Signal → WARTEN, wenn das Gegenlevel (M5-Pivot in Trade-
|
# Entry-Raum-Gate: Signal → WARTEN, wenn das Gegenlevel (M5-Pivot in Trade-
|
||||||
# Richtung) näher als X×ATR liegt (gemessen `backtest_entryroom.py` — Raum
|
# 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
|
# <0,6 in beiden Hälften negativ: Ertrag am Level gedeckelt, Kosten fressen
|
||||||
# den Rest). 0 = aus.
|
# den Rest). 0 = aus. **0,6→1,0 am 2026-07-24 (User: „baue alles ein"):**
|
||||||
"entry_room_atr": "0.6",
|
# 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-
|
# Tageszeit-Gate: Komma-Liste der Stunden (Berlin), zu denen die Wellen-
|
||||||
# Empfehlung WARTEN erzwingt. **Gemessener Default = 0–7,12,16** (Nacht-
|
# Empfehlung WARTEN erzwingt. **Gemessener Default = 0–7,12,16** (Nacht-
|
||||||
# Kostenfalle + 12/16 Uhr beidhälftig negativ, `backtest_hourly_split.py`).
|
# Kostenfalle + 12/16 Uhr beidhälftig negativ, `backtest_hourly_split.py`).
|
||||||
|
|||||||
+59
-1
@@ -947,6 +947,13 @@ class TradingEngine:
|
|||||||
# history.log_recommendation → flutet nicht).
|
# history.log_recommendation → flutet nicht).
|
||||||
try:
|
try:
|
||||||
adv = (self.agent.snapshot() or {}).get("advisory") or {}
|
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(
|
self.history.log_recommendation(
|
||||||
signal=rec.get("signal"), score=float(rec.get("score") or 0.0),
|
signal=rec.get("signal"), score=float(rec.get("score") or 0.0),
|
||||||
conf_pct=int(rec.get("conf_pct") or 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),
|
reversal=("rev" if "REV" in (rec.get("setup") or "") else None),
|
||||||
ai_sentiment=adv.get("bias"), ai_confidence=adv.get("confidence"),
|
ai_sentiment=adv.get("bias"), ai_confidence=adv.get("confidence"),
|
||||||
setup=rec.get("setup"), regime=None, rsi=s.get("rsi_m15"),
|
setup=rec.get("setup"), regime=None, rsi=s.get("rsi_m15"),
|
||||||
news_score=None)
|
news_score=_news)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(f"log_recommendation: {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)
|
# Kontext für Trade-Logging (set_open_context beim Öffnen)
|
||||||
self._last_rec = {
|
self._last_rec = {
|
||||||
"rec_signal": rec.get("signal"),
|
"rec_signal": rec.get("signal"),
|
||||||
@@ -1651,6 +1679,21 @@ class TradingEngine:
|
|||||||
# ══════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════
|
||||||
_SNAP_TTL_S = 0.8 # geteilter Snapshot gilt so lange als frisch
|
_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:
|
def snapshot_cached(self) -> dict:
|
||||||
"""Teilt EINEN frisch berechneten Snapshot über alle WS-Clients/HTTP-
|
"""Teilt EINEN frisch berechneten Snapshot über alle WS-Clients/HTTP-
|
||||||
Aufrufe (TTL ~0,8 s). Spart SQLite/Verdict-Last bei mehreren Geräten —
|
Aufrufe (TTL ~0,8 s). Spart SQLite/Verdict-Last bei mehreren Geräten —
|
||||||
@@ -1903,6 +1946,21 @@ class TradingEngine:
|
|||||||
"all": self._compact_stats("all")},
|
"all": self._compact_stats("all")},
|
||||||
"close_alert_count": self._close_alert_count,
|
"close_alert_count": self._close_alert_count,
|
||||||
"news_storm": self.news.storm_state(),
|
"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,
|
"sr_close_hint": sr_close_hint,
|
||||||
"stop_approach": stop_approach,
|
"stop_approach": stop_approach,
|
||||||
"auto_sr_close": self._auto_sr_close,
|
"auto_sr_close": self._auto_sr_close,
|
||||||
|
|||||||
@@ -138,6 +138,25 @@ SCHEMA = [
|
|||||||
correct INTEGER -- NULL bis ausgewertet, dann 0/1
|
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
|
# 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_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_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_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.
|
# Migrations: Spalten, die in alten DBs evtl. fehlen.
|
||||||
@@ -394,6 +414,61 @@ class HistoryLogger:
|
|||||||
"pending": pending,
|
"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
|
# STATISTIK-QUERIES
|
||||||
# ══════════════════════════════════════════
|
# ══════════════════════════════════════════
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ tf_select = heuristic
|
|||||||
tf_min = M5
|
tf_min = M5
|
||||||
tf_max = M30
|
tf_max = M30
|
||||||
breakout_k = 0.3
|
breakout_k = 0.3
|
||||||
entry_room_atr = 0.6
|
entry_room_atr = 1.0
|
||||||
dead_hours =
|
dead_hours =
|
||||||
auto_squeeze = true
|
auto_squeeze = true
|
||||||
auto_squeeze_skip_night = true
|
auto_squeeze_skip_night = true
|
||||||
|
|||||||
+52
-6
@@ -68,7 +68,7 @@ function beepRecommendation(sig, isNew) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Gesamtempfehlung: Konfidenz-Ring + Bias-Balken + Modul-Chips ───────────
|
// ── Gesamtempfehlung: Konfidenz-Ring + Bias-Balken + Modul-Chips ───────────
|
||||||
function renderVerdict(v) {
|
function renderVerdict(v, snap) {
|
||||||
const sig = v.headline || "—";
|
const sig = v.headline || "—";
|
||||||
const cls = sig === "LONG" ? "long" : sig === "SHORT" ? "short" : "";
|
const cls = sig === "LONG" ? "long" : sig === "SHORT" ? "short" : "";
|
||||||
const arr = sig === "LONG" ? "▲" : sig === "SHORT" ? "▼" : "◌";
|
const arr = sig === "LONG" ? "▲" : sig === "SHORT" ? "▼" : "◌";
|
||||||
@@ -89,9 +89,18 @@ function renderVerdict(v) {
|
|||||||
const bias = typeof v.bias === "number" ? v.bias : 0;
|
const bias = typeof v.bias === "number" ? v.bias : 0;
|
||||||
const strength = Math.abs(bias) >= 0.5 ? "stark" : Math.abs(bias) >= 0.2 ? "leicht" : "gemischt";
|
const strength = Math.abs(bias) >= 0.5 ? "stark" : Math.abs(bias) >= 0.2 ? "leicht" : "gemischt";
|
||||||
const dir = bias > 0.05 ? "LONG" : bias < -0.05 ? "SHORT" : "neutral";
|
const dir = bias > 0.05 ? "LONG" : bias < -0.05 ? "SHORT" : "neutral";
|
||||||
|
// Live-Kosten-Chip (Spread/ATR — gemessen Ø 0,265; Nacht 0,32–0,50 = Falle):
|
||||||
|
// reine Info, die die abgeschaltete Dead-Hours-Sperre ehrlich ersetzt.
|
||||||
|
const cr = snap && snap.cost_ratio;
|
||||||
|
let costTxt = "";
|
||||||
|
if (cr != null) {
|
||||||
|
const tag = cr < 0.20 ? "✅ günstig" : cr <= 0.32 ? "· normal" : "⚠ teuer";
|
||||||
|
costTxt = ` · Kosten ${fmt(cr, 2)}×ATR ${tag}`;
|
||||||
|
}
|
||||||
$("vd-consensus").textContent =
|
$("vd-consensus").textContent =
|
||||||
sig === "—" ? "keine Daten"
|
(sig === "—" ? "keine Daten"
|
||||||
: `Modul-Konsens: ${strength} ${dir} · ${agree}/${total} Module für ${sig}`;
|
: `Modul-Konsens: ${strength} ${dir} · ${agree}/${total} Module für ${sig}`)
|
||||||
|
+ costTxt;
|
||||||
$("vd-needle").style.left = ((bias + 1) / 2 * 100).toFixed(1) + "%";
|
$("vd-needle").style.left = ((bias + 1) / 2 * 100).toFixed(1) + "%";
|
||||||
|
|
||||||
// Modul-Chips
|
// Modul-Chips
|
||||||
@@ -284,7 +293,7 @@ function render(d) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Gesamtempfehlung (aggregiert) ───────
|
// ── Gesamtempfehlung (aggregiert) ───────
|
||||||
renderVerdict(d.verdict || {});
|
renderVerdict(d.verdict || {}, d);
|
||||||
renderStructure(d.structure);
|
renderStructure(d.structure);
|
||||||
renderStatsCompact(d.stats_compact, d.market);
|
renderStatsCompact(d.stats_compact, d.market);
|
||||||
renderGaps(d.gaps, d.market);
|
renderGaps(d.gaps, d.market);
|
||||||
@@ -518,6 +527,28 @@ function render(d) {
|
|||||||
stopEl.textContent = "";
|
stopEl.textContent = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// News-Konflikt-Chip (2026-07-24, realer Auslöser 23.07.: zwei Shorts gegen einen
|
||||||
|
// +0,98-Sentiment-Burst): |Score|≥0,5 UND Richtung gegen offene Position bzw.
|
||||||
|
// gegen das aktive Signal → Warnung. Reiner Kontext, Prädiktivität unbelegt.
|
||||||
|
const ncEl = $("news-conflict");
|
||||||
|
if (ncEl) {
|
||||||
|
const ns = d.news_sentiment;
|
||||||
|
let txt = "";
|
||||||
|
if (ns != null && Math.abs(ns) >= 0.5) {
|
||||||
|
const newsDir = ns > 0 ? "LONG" : "SHORT";
|
||||||
|
const newsWord = ns > 0 ? "bullisch" : "bärisch";
|
||||||
|
if (hasPos) {
|
||||||
|
const posDir = posLong ? "LONG" : "SHORT";
|
||||||
|
if (posDir !== newsDir)
|
||||||
|
txt = `📰 News-Flow stark ${newsWord} (${ns > 0 ? "+" : ""}${fmt(ns, 2)}) — steht gegen deine ${posDir}-Position`;
|
||||||
|
} else if (w.signal === "LONG" || w.signal === "SHORT") {
|
||||||
|
if (w.signal !== newsDir)
|
||||||
|
txt = `📰 News-Flow stark ${newsWord} (${ns > 0 ? "+" : ""}${fmt(ns, 2)}) — steht gegen das ${w.signal}-Signal`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (txt) { ncEl.className = "news-conflict warn"; ncEl.textContent = txt; }
|
||||||
|
else { ncEl.className = "news-conflict hidden"; ncEl.textContent = ""; }
|
||||||
|
}
|
||||||
// Schalter-Zustand: Auto-Close am S/R an/aus + Mindestgewinn
|
// Schalter-Zustand: Auto-Close am S/R an/aus + Mindestgewinn
|
||||||
const srBtn = $("srclose-btn");
|
const srBtn = $("srclose-btn");
|
||||||
if (srBtn) {
|
if (srBtn) {
|
||||||
@@ -735,11 +766,26 @@ document.querySelectorAll(".btn[data-side]").forEach(btn => {
|
|||||||
const info = recInfo(side); // WARTEN/Gegen-Signal → Hinweis, der
|
const info = recInfo(side); // WARTEN/Gegen-Signal → Hinweis, der
|
||||||
if (info.danger) { // bestätigt werden muss (kein Block)
|
if (info.danger) { // bestätigt werden muss (kein Block)
|
||||||
const sig = (lastSnap.wave_signal || {}).signal || "WARTEN";
|
const sig = (lastSnap.wave_signal || {}).signal || "WARTEN";
|
||||||
const head = (sig === "WARTEN" || sig === "—")
|
const isWait = (sig === "WARTEN" || sig === "—");
|
||||||
|
const head = isWait
|
||||||
? "ℹ Keine Empfehlung (WARTEN)"
|
? "ℹ Keine Empfehlung (WARTEN)"
|
||||||
: `ℹ Empfehlung steht GEGEN dich (${sig})`;
|
: `ℹ Empfehlung steht GEGEN dich (${sig})`;
|
||||||
|
// Ehrliche EIGENE Zahlen (letzte 40 Trades, Ausrichtungs-Split aus der DB):
|
||||||
|
// die gemessene Kern-Leckage sind genau diese diskretionären Abweichungen.
|
||||||
|
let statLine = "";
|
||||||
|
const al = lastSnap.alignment || null;
|
||||||
|
if (al) {
|
||||||
|
const grp = isWait ? al.none : al.against;
|
||||||
|
const ref = al.with;
|
||||||
|
const f = g => (g && g.n >= 5)
|
||||||
|
? `${g.wr}% Trefferquote · ${g.pnl >= 0 ? "+" : ""}${fmt(g.pnl, 0)} € (n${g.n})`
|
||||||
|
: null;
|
||||||
|
const own = f(grp), rf = f(ref);
|
||||||
|
if (own) statLine += `\n\n📊 Deine Trades ${isWait ? "OHNE Signal" : "GEGEN das Signal"}: ${own}`;
|
||||||
|
if (rf) statLine += `\n📊 Mit Signal: ${rf}`;
|
||||||
|
}
|
||||||
const ok = await modal(head,
|
const ok = await modal(head,
|
||||||
info.line + `\n\n${side.toUpperCase()} trotzdem eröffnen?`,
|
info.line + statLine + `\n\n${side.toUpperCase()} trotzdem eröffnen?`,
|
||||||
"confirm", true);
|
"confirm", true);
|
||||||
if (!ok) return; // Abbrechen = doch nicht; sonst läuft er
|
if (!ok) return; // Abbrechen = doch nicht; sonst läuft er
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -6,7 +6,7 @@
|
|||||||
<meta name="theme-color" content="#0d1117">
|
<meta name="theme-color" content="#0d1117">
|
||||||
<title>Oil · MT5</title>
|
<title>Oil · MT5</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
<link rel="stylesheet" href="style.css?v=113">
|
<link rel="stylesheet" href="style.css?v=114">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Modul-Menü (ganz oben) -->
|
<!-- Modul-Menü (ganz oben) -->
|
||||||
@@ -91,6 +91,7 @@
|
|||||||
<div class="cur-near" id="cur-near"></div>
|
<div class="cur-near" id="cur-near"></div>
|
||||||
<div class="pos-srhint hidden" id="pos-srhint"></div>
|
<div class="pos-srhint hidden" id="pos-srhint"></div>
|
||||||
<div class="pos-stophint hidden" id="pos-stophint"></div>
|
<div class="pos-stophint hidden" id="pos-stophint"></div>
|
||||||
|
<div class="news-conflict hidden" id="news-conflict"></div>
|
||||||
<div class="reasons" id="wave-reasons"></div>
|
<div class="reasons" id="wave-reasons"></div>
|
||||||
<div class="kv"><span>Setup</span><b id="wave-setup">—</b></div>
|
<div class="kv"><span>Setup</span><b id="wave-setup">—</b></div>
|
||||||
<div class="kv"><span>Lauf</span><b id="wave-move">—</b></div>
|
<div class="kv"><span>Lauf</span><b id="wave-move">—</b></div>
|
||||||
@@ -216,7 +217,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="toast" class="toast hidden"></div>
|
<div id="toast" class="toast hidden"></div>
|
||||||
|
|
||||||
<script src="lightweight-charts.standalone.production.js?v=113"></script>
|
<script src="lightweight-charts.standalone.production.js?v=114"></script>
|
||||||
<script src="app.js?v=113"></script>
|
<script src="app.js?v=114"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+5
-1
@@ -167,7 +167,8 @@ main{display:grid;grid-template-columns:1fr 1fr;gap:11px;padding:11px}
|
|||||||
|
|
||||||
/* ── Meldungen (EINHEITLICHES Design: Box + farbiger Links-Akzent) ─────────── */
|
/* ── Meldungen (EINHEITLICHES Design: Box + farbiger Links-Akzent) ─────────── */
|
||||||
#card-pos .squeeze-ind, #card-pos .bounce-ind, #card-pos .cur-near,
|
#card-pos .squeeze-ind, #card-pos .bounce-ind, #card-pos .cur-near,
|
||||||
#card-pos .pos-srhint, #card-pos .pos-stophint, #card-pos .reasons{
|
#card-pos .pos-srhint, #card-pos .pos-stophint, #card-pos .news-conflict,
|
||||||
|
#card-pos .reasons{
|
||||||
display:block; margin:0 0 7px; padding:8px 11px; border-radius:var(--r-sm);
|
display:block; margin:0 0 7px; padding:8px 11px; border-radius:var(--r-sm);
|
||||||
background:var(--surface-2); border:1px solid var(--border);
|
background:var(--surface-2); border:1px solid var(--border);
|
||||||
border-left:3px solid var(--line);
|
border-left:3px solid var(--line);
|
||||||
@@ -179,6 +180,9 @@ main{display:grid;grid-template-columns:1fr 1fr;gap:11px;padding:11px}
|
|||||||
/* Gegen-/Stop-Seiten-Hinweis (reine Anzeige): hält = grün, bricht = rot */
|
/* Gegen-/Stop-Seiten-Hinweis (reine Anzeige): hält = grün, bricht = rot */
|
||||||
#card-pos .pos-stophint.hold{color:var(--up); border-left-color:var(--up)}
|
#card-pos .pos-stophint.hold{color:var(--up); border-left-color:var(--up)}
|
||||||
#card-pos .pos-stophint.break{color:var(--down); border-left-color:var(--down)}
|
#card-pos .pos-stophint.break{color:var(--down); border-left-color:var(--down)}
|
||||||
|
/* News-Konflikt (Flow steht gegen Signal/Position) — bernstein wie Warnungen */
|
||||||
|
#card-pos .news-conflict.warn{color:var(--accent); border-left-color:var(--accent)}
|
||||||
|
#card-pos .news-conflict.hidden, #card-pos .news-conflict:empty{display:none}
|
||||||
/* Farbvarianten — Links-Akzent + Textfarbe (Fläche bleibt einheitlich) */
|
/* Farbvarianten — Links-Akzent + Textfarbe (Fläche bleibt einheitlich) */
|
||||||
#card-pos .squeeze-ind.idle, #card-pos .bounce-ind.idle,
|
#card-pos .squeeze-ind.idle, #card-pos .bounce-ind.idle,
|
||||||
#card-pos .reasons{color:var(--dim)}
|
#card-pos .reasons{color:var(--dim)}
|
||||||
|
|||||||
Reference in New Issue
Block a user