Gegen-/Stop-Seiten-Anzeige P(Durchbruch) (stop_approach, v=109)
Zeigt die Durchbruchwahrscheinlichkeit fuer das Level, dem sich der Kurs auf der Gegen-/Stop-Seite der offenen Position naehert (LONG->Support darunter, SHORT->Resistance darueber) — reine Anzeige, kein Auto-Close. Vorher per backtest_pbreak_calibration.py belegt: Modell haelt auf der Gegen-Seite out-of-sample (AUC 0,68/0,72 in beiden Haelften), niedriger P-Bereich kalibriert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6318817ac1
commit
027995d1e7
@@ -559,6 +559,25 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
|
||||
mit synthetischen Szenarien (break/bounce/Gegenteil-falsch/Timeout) — alle 4 exakt
|
||||
wie erwartet. **Reine Messung, kein Signal-/Exit-Eingriff** — noch keine UI-Karte
|
||||
(Datensammlung; B4-artige Auswertung später möglich, wenn genug Zeilen vorliegen).
|
||||
**Gegen-/Stop-Seiten-Anzeige `stop_approach` (2026-07-24, User-Frage „Kurs nähert
|
||||
sich Support, aber keine Durchbruchwahrscheinlichkeit angezeigt"):** Der reguläre
|
||||
`sr_close_hint` beobachtet nur das **Ziel-Level** (LONG→Resistance, SHORT→Support)
|
||||
und wirkt nur im Plus — nähert sich der Kurs dem **Gegen-/Stop-Level** (LONG→Support
|
||||
darunter, SHORT→Resistance darüber, typ. Verlust-Trade), war die App bewusst still.
|
||||
Neu: `engine._stop_approach_hint` liefert für GENAU dieses Level eine **reine
|
||||
Anzeige** von P(Durchbruch) — Break-Richtung `-d` (gegen den Trade), Feature-/
|
||||
60-s-Glättung (`_stop_pb_state`) identisch zu `_sr_close_hint`. **KEIN Auto-Close.**
|
||||
Snapshot `stop_approach {level, side, p_break, at_level, gap_atr, confirm, reject}`,
|
||||
Frontend `#pos-stophint` (grün „Level hält wahrscheinlich" bei P<50, rot „bricht
|
||||
wahrscheinlich durch" bei ≥50; nur wenn KEIN Ziel-`sr_close_hint` läuft; v=109).
|
||||
**Vorher gemessen (`backtest_pbreak_calibration.py`, 2026-07-24, Gate „erst messen,
|
||||
dann bauen"):** Das kalibrierte P(break)-Modell (auf Ziel-Seite trainiert) hält
|
||||
auch auf der **Gegen-/Stop-Seite** — AUC **0,68/0,72** out-of-sample in BEIDEN
|
||||
Hälften (≈ Ziel-Seite 0,67/0,71), im niedrigen P-Bereich sauber kalibriert (der
|
||||
praktisch wichtige „hält der Support?"-Fall). ⚠ Einzige Schwäche: im HOHEN Bereich
|
||||
(P≥0,5) GEGEN den Trend unterschätzt es die Bruch-Rate ~10–13 Pp (predicted 58 % →
|
||||
echt 71 %) — also **konservativ** (warnt eher zu wenig vor einem Bruch), kein
|
||||
Kalibrierungs-Bruch. Deshalb reine Anzeige, kein Trade-Trigger.
|
||||
**Anti-Flattern (2026-07-14, gemessen `analyze_pbreak_flicker.py`:** mom3 der
|
||||
laufenden M5-Kerze ließ P 7–15 Pp/Bar springen, Max 77 Pp; 8 % der Annäherungen
|
||||
hatten ≥2 Schwellen-Flips): (1) **P zeit-geglättet** (~60-s-EMA in `_pb_state`,
|
||||
@@ -1084,7 +1103,7 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
|
||||
Signal → **Hinweis-Dialog „trotzdem eröffnen?"** (überschreibbar). CLOSE bei
|
||||
**negativem** P&L → Sicherheitsabfrage; im Plus/Breakeven direkt. Sonst keine
|
||||
Token-Dialoge (`require_token=false`). Audio braucht 1× Nutzer-Tap.
|
||||
- Asset-Version aktuell **v=108** (in `web/index.html` hochzählen, siehe Workflows).
|
||||
- Asset-Version aktuell **v=109** (in `web/index.html` hochzählen, siehe Workflows).
|
||||
|
||||
## Web-API (server.py)
|
||||
`GET /api/snapshot` (inkl. `verdict`-Aggregat) `/api/health` `/api/logs` `/api/stats` `/api/news` `/api/bars?tf=&n=` (Charts, read-only) `/api/squeeze_monitor` (B4-Monitor, read-only, Engine-Cache 60 s) · `WS /ws` ·
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Kalibrierungs-Test des P(break)-Modells auf der GEGEN-/STOP-Seite (User-Frage:
|
||||
Kurs naehert sich einem Level GEGEN Trade/Trend — ist die P(break)-Zahl dort
|
||||
belastbar?). Vorgehen wie backtest_srclose_prob.py:
|
||||
|
||||
- Modell auf H1 ZIEL-Seiten-Touches trainieren (identisch zum Live-Modell).
|
||||
- Auswertung auf H1 UND H2, getrennt nach:
|
||||
* Quelle: ZIEL-Seite (mit-Trade, wie trainiert) vs STOP-Seite (gegen-Trade)
|
||||
* wt: mit-Trend (1) vs gegen-Trend (0)
|
||||
- Metriken je Gruppe: Kalibrierung (Ø-P vs echte Break-Rate je P-Bin) + AUC + n.
|
||||
Break/Bounce-Definition IDENTISCH zum Training: _BRK_ATR=0.5, _BRK_W=12 (=60min).
|
||||
"""
|
||||
import sys
|
||||
import numpy as np
|
||||
import MetaTrader5 as mt5
|
||||
from core.analysis import calc_trend_angle
|
||||
from core.wave_rec import (_EMA_FAST, _EMA_SLOW, _N_BARS, _ANGLE_LR, _ANGLE_DEAD,
|
||||
_REVERSAL_STRETCH, _STRETCH_MAX)
|
||||
_MAXH=200; _ATRMIN=0.12; _SL_ATR=2.0
|
||||
_PIV_K=3; _LOOKBACK=300; _BRK_W=12; _BRK_ATR=0.5
|
||||
|
||||
def _ema_series(v,p):
|
||||
k=2.0/(p+1); o=[]; e=v[0]
|
||||
for i,x in enumerate(v): e=x if i==0 else x*k+e*(1-k); o.append(e)
|
||||
return o
|
||||
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 _signal_dir(i,C,EF,ES,AT):
|
||||
atr=AT[i]
|
||||
if not atr or atr<=0: return 0,None
|
||||
atr=max(atr,_ATRMIN); es=ES[i]; ef=EF[i]
|
||||
stretch=(C[i]-es)/atr
|
||||
ang=calc_trend_angle(C[i-_ANGLE_LR-2:i],_ANGLE_LR); ad=ang-90.0
|
||||
if stretch<=-_REVERSAL_STRETCH and ad>=_ANGLE_DEAD: return 1,atr
|
||||
if stretch>=_REVERSAL_STRETCH and ad<=-_ANGLE_DEAD: return -1,atr
|
||||
if abs(stretch)<_STRETCH_MAX: return (1 if ef>es else -1 if ef<es else 0),atr
|
||||
return 0,atr
|
||||
|
||||
def _pivots(i,H,L):
|
||||
phis,plos=[],[]
|
||||
for j in range(i-_LOOKBACK+_PIV_K, i-_PIV_K):
|
||||
if H[j]==max(H[j-_PIV_K:j+_PIV_K+1]): phis.append(H[j])
|
||||
if L[j]==min(L[j-_PIV_K:j+_PIV_K+1]): plos.append(L[j])
|
||||
return phis,plos
|
||||
|
||||
def _touch_and_break(i,entry,dd,atr,level,H,L,C,EF,ES):
|
||||
"""Sucht ab i+1 den ersten Touch des Levels in Richtung dd (dd=Break-Richtung),
|
||||
dann Break/Bounce per _BRK_ATR/_BRK_W. → dict(feat, brk) oder None."""
|
||||
jt=None
|
||||
for j in range(i+1, min(i+_MAXH, len(C)-_BRK_W)):
|
||||
# Touch = Kurs erreicht das Level in Break-Richtung dd
|
||||
if (H[j]>=level) if dd>0 else (L[j]<=level): jt=j; break
|
||||
# Abbruch, wenn Kurs vorher 2*atr in Gegenrichtung (weg vom Level) laeuft
|
||||
if ((C[j]-entry)*(-dd)) >= 2.0*atr: break
|
||||
if jt is None: return None
|
||||
up=level+dd*_BRK_ATR*atr; dn=level-dd*_BRK_ATR*atr; brk=None
|
||||
for j in range(jt, min(jt+_BRK_W, len(H))):
|
||||
if (H[j]>=up) if dd>0 else (L[j]<=up): brk=1.0; break
|
||||
if (L[j]<=dn) if dd>0 else (H[j]>=dn): brk=0.0; break
|
||||
if brk is None: brk=0.0 # Timeout = Bounce (wie im Training)
|
||||
feat=[(C[jt]-C[max(0,jt-6)])*dd/atr,(C[jt]-C[max(0,jt-3)])*dd/atr,
|
||||
1.0 if (EF[jt]-ES[jt])*dd>0 else 0.0, abs(level-entry)/atr]
|
||||
return {"feat":feat,"brk":brk,"wt":feat[2]}
|
||||
|
||||
def collect(a,b,H,L,C,EF,ES,AT,side):
|
||||
"""side='target' → mit-Trade Ziel-Level (wie Training); 'stop' → Gegen-Level."""
|
||||
out=[]
|
||||
for i in range(max(a,_N_BARS,_LOOKBACK), min(b,len(C)-_MAXH-1)):
|
||||
d,atr=_signal_dir(i,C,EF,ES,AT)
|
||||
if not d: continue
|
||||
entry=C[i]; phis,plos=_pivots(i,H,L)
|
||||
if side=="target":
|
||||
if d>0:
|
||||
cands=[p for p in phis if p>entry+0.3*atr]; level=min(cands) if cands else None
|
||||
else:
|
||||
cands=[p for p in plos if p<entry-0.3*atr]; level=max(cands) if cands else None
|
||||
dd=d
|
||||
else: # stop-Seite: Gegen-Level, Break-Richtung = -d
|
||||
if d>0:
|
||||
cands=[p for p in plos if p<entry-0.3*atr]; level=max(cands) if cands else None
|
||||
else:
|
||||
cands=[p for p in phis if p>entry+0.3*atr]; level=min(cands) if cands else None
|
||||
dd=-d
|
||||
if level is None: continue
|
||||
r=_touch_and_break(i,entry,dd,atr,level,H,L,C,EF,ES)
|
||||
if r: out.append(r)
|
||||
return out
|
||||
|
||||
def fit_logreg(X,y,iters=3000,lr=0.3):
|
||||
n,m=X.shape; Xb=np.hstack([np.ones((n,1)),X]); w=np.zeros(m+1)
|
||||
for _ in range(iters):
|
||||
p=1/(1+np.exp(-(Xb@w))); w-=lr*(Xb.T@(p-y))/n
|
||||
return w
|
||||
|
||||
def auc(p,y):
|
||||
p=np.asarray(p); y=np.asarray(y)
|
||||
pos=p[y==1]; neg=p[y==0]
|
||||
if len(pos)==0 or len(neg)==0: return float('nan')
|
||||
# Mann-Whitney via Rang
|
||||
order=np.argsort(np.concatenate([pos,neg]))
|
||||
ranks=np.empty_like(order,dtype=float); ranks[order]=np.arange(1,len(order)+1)
|
||||
r_pos=ranks[:len(pos)].sum()
|
||||
return (r_pos-len(pos)*(len(pos)+1)/2)/(len(pos)*len(neg))
|
||||
|
||||
def calib_table(name,ev,pf):
|
||||
if not ev:
|
||||
print(f" {name:<28} (keine Events)"); return
|
||||
P=np.array([pf(e) for e in ev]); Y=np.array([e["brk"] for e in ev])
|
||||
a=auc(P,Y); base=Y.mean()
|
||||
print(f" {name:<28} n={len(ev):>5} Break-Basisrate={base*100:>4.0f}% AUC={a:.2f}")
|
||||
bins=[(0,0.3),(0.3,0.5),(0.5,0.7),(0.7,1.01)]
|
||||
for lo,hi in bins:
|
||||
m=(P>=lo)&(P<hi)
|
||||
if m.sum()==0: continue
|
||||
print(f" P {lo:.1f}-{hi:.1f}: vorhergesagt Ø{P[m].mean()*100:>4.0f}% → echt {Y[m].mean()*100:>4.0f}% (n{m.sum()})")
|
||||
|
||||
def main():
|
||||
n=int(sys.argv[1]) if len(sys.argv)>1 else 80000
|
||||
mt5.initialize(); sym=None
|
||||
for c in ("SpotCrude","USOIL","WTI","XTIUSD"):
|
||||
if mt5.symbol_info(c): sym=c; break
|
||||
bars=None
|
||||
for req in (n,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]
|
||||
EF=_ema_series(C,_EMA_FAST); ES=_ema_series(C,_EMA_SLOW); AT=_atr_series(H,L,C)
|
||||
mid=len(C)//2
|
||||
print("="*94)
|
||||
print(f" P(break)-Kalibrierung Ziel- vs Stop-Seite — {sym} M5, {len(bars)} Bars")
|
||||
print("="*94)
|
||||
tgt1=collect(_N_BARS,mid,H,L,C,EF,ES,AT,"target")
|
||||
tgt2=collect(mid,len(C),H,L,C,EF,ES,AT,"target")
|
||||
stp1=collect(_N_BARS,mid,H,L,C,EF,ES,AT,"stop")
|
||||
stp2=collect(mid,len(C),H,L,C,EF,ES,AT,"stop")
|
||||
# Modell auf H1-ZIEL-Touches trainieren (identisch zum Live-Modell)
|
||||
X1=np.array([e["feat"] for e in tgt1]); y1=np.array([e["brk"] for e in tgt1])
|
||||
mu=X1.mean(0); sd=X1.std(0)+1e-9
|
||||
w=fit_logreg((X1-mu)/sd,y1)
|
||||
def pf(e):
|
||||
x=(np.array(e["feat"])-mu)/sd
|
||||
return 1/(1+np.exp(-(w@np.concatenate([[1.0],x]))))
|
||||
print(f" Modell (auf H1-Ziel trainiert): mom6={w[1]:+.2f} mom3={w[2]:+.2f} wt={w[3]:+.2f} dist={w[4]:+.2f}\n")
|
||||
for lbl,tgt,stp in (("H1 (Trainings-Halfte)",tgt1,stp1),("H2 (OUT-OF-SAMPLE)",tgt2,stp2)):
|
||||
print(f"── {lbl} ──")
|
||||
calib_table("ZIEL-Seite (mit-Trade)",tgt,pf)
|
||||
calib_table("STOP-Seite (gegen-Trade)",stp,pf)
|
||||
# STOP-Seite zusaetzlich nach wt gesplittet (der User-Fall ist wt=0)
|
||||
calib_table(" davon gegen-Trend (wt=0)",[e for e in stp if e["wt"]==0.0],pf)
|
||||
calib_table(" davon mit-Trend (wt=1)",[e for e in stp if e["wt"]==1.0],pf)
|
||||
print()
|
||||
print(" Kalibriert = 'vorhergesagt Ø' ≈ 'echt' in jedem Bin, UND das in H2 (out-of-sample).")
|
||||
|
||||
if __name__=="__main__":
|
||||
main()
|
||||
@@ -236,6 +236,7 @@ class TradingEngine:
|
||||
except (TypeError, ValueError):
|
||||
self._sr_close_min_gain_pct = 1.0
|
||||
self._pb_state = None # geglättetes P(break) + Hysterese-Zone je Level
|
||||
self._stop_pb_state = None # dito für die Gegen-/Stop-Seiten-Anzeige
|
||||
# (gemessen `analyze_pbreak_flicker.py`: mom3 der
|
||||
# laufenden Kerze ließ P ~7–15 Pp je M5-Bar springen)
|
||||
# UI-Einstellungen neustart-fest (Fix 2026-07-19): Toggles/Werte, die über
|
||||
@@ -1862,6 +1863,7 @@ class TradingEngine:
|
||||
day_pl = {"realized": round(day_realized, 2), "open": round(open_pnl, 2),
|
||||
"total": round(day_realized + open_pnl, 2)}
|
||||
sr_close_hint = self._sr_close_hint(market, position, wave_snap)
|
||||
stop_approach = self._stop_approach_hint(market, position, wave_snap)
|
||||
return {
|
||||
"ts": time.time(),
|
||||
"connected": self._connected,
|
||||
@@ -1902,6 +1904,7 @@ class TradingEngine:
|
||||
"close_alert_count": self._close_alert_count,
|
||||
"news_storm": self.news.storm_state(),
|
||||
"sr_close_hint": sr_close_hint,
|
||||
"stop_approach": stop_approach,
|
||||
"auto_sr_close": self._auto_sr_close,
|
||||
"sr_close_min_gain": self._sr_close_min_gain,
|
||||
"sr_close_pbreak": self._sr_close_pbreak,
|
||||
@@ -2016,3 +2019,63 @@ class TradingEngine:
|
||||
except Exception as e:
|
||||
log.debug(f"sr_close_hint: {e}")
|
||||
return None
|
||||
|
||||
def _stop_approach_hint(self, market, position, wave_snap) -> dict | None:
|
||||
"""REINE ANZEIGE: P(Durchbruch) für das Level, dem sich der Kurs auf der
|
||||
**Gegen-/Stop-Seite** der offenen Position nähert (LONG → Support darunter,
|
||||
SHORT → Resistance darüber). Beantwortet die User-Frage „mein Trade läuft auf
|
||||
ein Level zu — bricht es durch (Verlust läuft) oder prallt es ab (erholt sich)?".
|
||||
NICHT der S/R-Auto-Close (der wirkt nur auf der Ziel-Seite im Plus) — hier wird
|
||||
NICHTS geschlossen. Kalibrierung der Gegen-Seite gemessen
|
||||
(`backtest_pbreak_calibration.py`, 2026-07-24): AUC 0,68/0,72 out-of-sample in
|
||||
beiden Hälften, im niedrigen P-Bereich sauber kalibriert; im hohen (≥0,5) gegen
|
||||
den Trend leicht KONSERVATIV (unterschätzt die Bruch-Rate ~10–13 Pp) → die Zahl
|
||||
ist eine belastbare, eher vorsichtige Schätzung. Feature-/Glättungs-Logik
|
||||
identisch zu `_sr_close_hint`, nur Level = Gegen-Level und Richtung = -d."""
|
||||
try:
|
||||
if not position.get("ticket"):
|
||||
self._stop_pb_state = None
|
||||
return None
|
||||
is_long = position.get("order_type") == 0
|
||||
bid = (market or {}).get("bid")
|
||||
pf = (wave_snap or {}).get("pb_feats") or {}
|
||||
atr = pf.get("atr"); mom6 = pf.get("mom6"); mom3 = pf.get("mom3")
|
||||
ediff = pf.get("ema_diff")
|
||||
entry = position.get("entry_price") or bid
|
||||
dl = self._draw_levels()
|
||||
# Gegen-Level: LONG → Support darunter, SHORT → Resistance darüber
|
||||
if is_long:
|
||||
level = (dl.get("sup") or [None])[0]
|
||||
else:
|
||||
level = (dl.get("res") or [None])[0]
|
||||
if not (bid and atr and level) or mom6 is None or mom3 is None or ediff is None:
|
||||
self._stop_pb_state = None
|
||||
return None
|
||||
dd = -1 if is_long else 1 # Break-Richtung GEGEN den Trade
|
||||
gap = (level - bid) * dd # >0 solange Kurs noch vor dem Level
|
||||
if gap <= 0 or gap > 0.6 * atr: # weit weg / schon durch → kein Hinweis
|
||||
self._stop_pb_state = None
|
||||
return None
|
||||
wt = 1.0 if ediff * dd > 0 else 0.0
|
||||
dist = abs(level - entry) / atr
|
||||
p_raw = _p_break(mom6 * dd, mom3 * dd, wt, dist)
|
||||
import math as _m
|
||||
now = time.time(); st = self._stop_pb_state
|
||||
if not st or abs(st["level"] - level) > 1e-9:
|
||||
st = {"level": level, "p": p_raw, "ts": now}
|
||||
else:
|
||||
a = 1.0 - _m.exp(-max(0.0, now - st["ts"]) / 60.0)
|
||||
st["p"] += a * (p_raw - st["p"]); st["ts"] = now
|
||||
self._stop_pb_state = st
|
||||
p_s = st["p"]
|
||||
confirm = round(level + dd * 0.5 * atr, 3) # Durchbruch bestätigt ab
|
||||
reject = round(level - dd * 0.5 * atr, 3) # Abprall bestätigt ab
|
||||
return {"level": round(level, 3),
|
||||
"side": ("sup" if is_long else "res"),
|
||||
"p_break": round(100 * p_s),
|
||||
"at_level": gap <= 0.15 * atr,
|
||||
"gap_atr": round(gap / atr, 2),
|
||||
"confirm": confirm, "reject": reject}
|
||||
except Exception as e:
|
||||
log.debug(f"stop_approach_hint: {e}")
|
||||
return None
|
||||
|
||||
+23
@@ -495,6 +495,29 @@ function render(d) {
|
||||
hintEl.textContent = "";
|
||||
}
|
||||
}
|
||||
// Gegen-/Stop-Seiten-Hinweis (REINE ANZEIGE, kein Auto-Close): Kurs läuft auf das
|
||||
// Level GEGEN die Position zu → bricht es durch (Verlust läuft) oder prallt es ab?
|
||||
// P(break)-Modell auf der Gegen-Seite gemessen kalibriert (AUC 0,72 o.o.s.),
|
||||
// im hohen Bereich leicht konservativ. Nur zeigen, wenn KEIN Ziel-Level-Hint läuft.
|
||||
const stopEl = $("pos-stophint");
|
||||
if (stopEl) {
|
||||
const sa = d.stop_approach || null;
|
||||
if (hasPos && sa && !srHint) {
|
||||
const lvlName = sa.side === "sup" ? "Support" : "Widerstand";
|
||||
const dir = sa.side === "sup" ? "nach unten" : "nach oben";
|
||||
const confirmTxt = sa.confirm != null ? ` (Bruch ab ${fmt(sa.confirm, 3)})` : "";
|
||||
const holds = sa.p_break < 50;
|
||||
stopEl.className = "pos-stophint " + (holds ? "hold" : "break");
|
||||
const lead = sa.at_level ? `Am ${lvlName}` : `Nähert sich ${lvlName}`;
|
||||
stopEl.textContent =
|
||||
`ℹ️ ${lead} ${fmt(sa.level, 3)} (${sa.gap_atr}×ATR) · Durchbruch ${dir} ` +
|
||||
`${sa.p_break}%${confirmTxt} → ` +
|
||||
(holds ? "Level hält wahrscheinlich" : "bricht wahrscheinlich durch");
|
||||
} else {
|
||||
stopEl.className = "pos-stophint hidden";
|
||||
stopEl.textContent = "";
|
||||
}
|
||||
}
|
||||
// Schalter-Zustand: Auto-Close am S/R an/aus + Mindestgewinn
|
||||
const srBtn = $("srclose-btn");
|
||||
if (srBtn) {
|
||||
|
||||
+3
-2
@@ -6,7 +6,7 @@
|
||||
<meta name="theme-color" content="#0d1117">
|
||||
<title>Oil · MT5</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="stylesheet" href="style.css?v=108">
|
||||
<link rel="stylesheet" href="style.css?v=109">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Modul-Menü (ganz oben) -->
|
||||
@@ -90,6 +90,7 @@
|
||||
<div class="bounce-ind" id="wave-bounce"></div>
|
||||
<div class="cur-near" id="cur-near"></div>
|
||||
<div class="pos-srhint hidden" id="pos-srhint"></div>
|
||||
<div class="pos-stophint hidden" id="pos-stophint"></div>
|
||||
<div class="reasons" id="wave-reasons"></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>
|
||||
@@ -216,6 +217,6 @@
|
||||
<div id="toast" class="toast hidden"></div>
|
||||
|
||||
<script src="lightweight-charts.standalone.production.js?v=108"></script>
|
||||
<script src="app.js?v=108"></script>
|
||||
<script src="app.js?v=109"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+6
-3
@@ -165,15 +165,18 @@ main{display:grid;grid-template-columns:1fr 1fr;gap:11px;padding:11px}
|
||||
|
||||
/* ── Meldungen (EINHEITLICHES Design: Box + farbiger Links-Akzent) ─────────── */
|
||||
#card-pos .squeeze-ind, #card-pos .bounce-ind, #card-pos .cur-near,
|
||||
#card-pos .pos-srhint, #card-pos .reasons{
|
||||
#card-pos .pos-srhint, #card-pos .pos-stophint, #card-pos .reasons{
|
||||
display:block; margin:0 0 7px; padding:8px 11px; border-radius:var(--r-sm);
|
||||
background:var(--surface-2); border:1px solid var(--border);
|
||||
border-left:3px solid var(--line);
|
||||
font-size:12px; line-height:1.4; font-weight:600; letter-spacing:0; box-shadow:none;
|
||||
}
|
||||
/* leere Meldungen ausblenden (cur-near/srhint/reasons ohne Inhalt) */
|
||||
#card-pos .cur-near:empty, #card-pos .pos-srhint:empty,
|
||||
#card-pos .pos-srhint.hidden, #card-pos .reasons:empty{display:none}
|
||||
#card-pos .cur-near:empty, #card-pos .pos-srhint:empty, #card-pos .pos-stophint:empty,
|
||||
#card-pos .pos-srhint.hidden, #card-pos .pos-stophint.hidden, #card-pos .reasons:empty{display:none}
|
||||
/* 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.break{color:var(--down); border-left-color:var(--down)}
|
||||
/* Farbvarianten — Links-Akzent + Textfarbe (Fläche bleibt einheitlich) */
|
||||
#card-pos .squeeze-ind.idle, #card-pos .bounce-ind.idle,
|
||||
#card-pos .reasons{color:var(--dim)}
|
||||
|
||||
Reference in New Issue
Block a user