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:
Axel Hocks
2026-07-24 20:57:56 +02:00
co-authored by Claude Opus 4.8
parent c3c6a8ce5f
commit d61c4a3634
10 changed files with 544 additions and 21 deletions
+53 -7
View File
@@ -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 **4054 % = negativer Edge** (0,025), ≥55 % = +0,058; (`backtest_conf.py`): Band **4054 % = 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 (MaiJun)
perfekt monoton (conf 85100: WR 68 %/ØR +1,13; <55 klar negativ), **H2 (JunJul)
komplett INVERTIERT** (conf 85100: 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` = (askbid)/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,320,50 = Falle, 1517 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`.
+119
View File
@@ -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()
+171
View File
@@ -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
View File
@@ -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 = 07,12,16** (Nacht- # Empfehlung WARTEN erzwingt. **Gemessener Default = 07,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
View File
@@ -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,320,50 =
# Kostenfalle, 1517 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,
+75
View File
@@ -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
# ══════════════════════════════════════════ # ══════════════════════════════════════════
+1 -1
View File
@@ -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
View File
@@ -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,320,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
View File
@@ -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
View File
@@ -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)}