UI aufgeraeumt + Telegram auf Closes reduziert (v=177)

Vier User-Vorgaben:
1. Einsatz-Felder entfernt. Backend bleibt vollstaendig -- Folge: der
   Prozentsatz ist ohne UI nur ueber ini + Neustart aenderbar.
2. BRK-Schalter entfernt, dafuer STOPP (Circuit-Breaker). Bewusster Tausch mit
   Konsequenz: der An/Aus-Schalter des AUTONOMEN Pfads ist aus der App weg (nur
   noch POST bzw. ini), sein Zustand bleibt in der Breakout-Karte sichtbar.
   Dafuer ist die Notbremse bedienbar, die am 17.08. real ausgeloest hat.
   set_circuit_breaker + POST /api/circuit + runtime_state (cb_limit_pct,
   cb_remember -- ohne das Gedaechtnis waere die Prozentzahl nach einmal
   Ausschalten fuer immer weg).
3. Telegram nur noch bei geschlossenen Trades: EINE Filterstelle vor
   _send_telegram_roh statt 17 Einzeleingriffe, _TG_ERLAUBT = ('Trade
   geschlossen',). Umkehren = Liste erweitern. Tages-/Wochenreport wird nur auf
   dem Telegram-Weg still, die E-Mail laeuft weiter.
   EINE Ausnahme mit Bypass: '<ok_word> FEHLGESCHLAGEN' beim Notfall-Close --
   dort ist die Position NICHT zu und es braucht eine Handeingabe.

Verifiziert: /api/circuit vorher 405, nachher 200; Toggle aus->an->8 %/57,94 EUR
persistiert; 85 Tests, node --check, check_nfalle gruen; 0 verwaiste
JS-Referenzen; genau eine Instanz je Port.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-19 17:03:30 +02:00
co-authored by Claude Opus 5
parent 32c064a1a6
commit e48ad80c0a
8 changed files with 5907 additions and 8425 deletions
-8302
View File
File diff suppressed because it is too large Load Diff
+77 -2
View File
@@ -38,7 +38,7 @@ from core.wave_rec import WaveRecommender
from core.elliott import ElliottAnalyzer from core.elliott import ElliottAnalyzer
from core.agent import TradingAgent from core.agent import TradingAgent
from core.news import NewsFetcher, NewsTranslator from core.news import NewsFetcher, NewsTranslator
from core.notify import send_telegram from core.notify import send_telegram as _send_telegram_roh
from core.mailer import send_graph_mail from core.mailer import send_graph_mail
from core.mt5_utils import mt5_lock from core.mt5_utils import mt5_lock
from core.market_hours import session_state from core.market_hours import session_state
@@ -166,6 +166,40 @@ def _p_break(mom6: float, mom3: float, wt: float, dist: float) -> float:
return 1.0 / (1.0 + math.exp(-max(-30.0, min(30.0, z)))) return 1.0 / (1.0 + math.exp(-max(-30.0, min(30.0, z))))
# --- Telegram-Gate (User-Vorgabe 2026-08-19) --------------------------------
# "entferne alle telegram benachrichtigungen. nur noch benachrichtigungen ueber
# geschlossene trades."
#
# BEWUSST EINE STELLE statt 17 Einzeleingriffe: die Sender bleiben stehen
# (Entry-Meldungen, Reports, Alarme), nur der Versand wird gefiltert. Kleiner
# Diff, trivial umkehrbar (Liste erweitern = wieder an) -- und beim
# Zurueckdrehen muessen nicht 17 Stellen einzeln wieder stimmen.
#
# Der TAGES- und WOCHENREPORT wird dadurch nur auf dem TELEGRAM-Weg still --
# die E-Mail laeuft weiter (er geht ohnehin an beide). Engste Lesart der
# Vorgabe: sie nennt ausdruecklich Telegram.
#
# EINE AUSNAHME mit Bypass: "<ok_word> FEHLGESCHLAGEN" beim Notfall-/
# Gewinn-Close. Dort ist die Position NICHT zu und es braucht eine
# Handeingabe -- Stille genau dort waere die gefaehrlichste Variante.
_TG_ERLAUBT = ("Trade geschlossen",)
def send_telegram(text, *a, **kw):
"""Gefilterter Versand -- nur Meldungen aus `_TG_ERLAUBT` gehen raus.
Rueckgabe False fuer Unterdruecktes, damit Aufrufer, die den Erfolg
auswerten (z. B. der Report-Versand-Marker), nichts faelschlich als
'zugestellt' verbuchen."""
try:
if not any(k in (text or "") for k in _TG_ERLAUBT):
return False
except Exception:
return False
return _send_telegram_roh(text, *a, **kw)
class TradingEngine: class TradingEngine:
"""Headless-Pendant zu OilWidget: hält die core-Objekte, fährt die """Headless-Pendant zu OilWidget: hält die core-Objekte, fährt die
Refresh-Loops als Threads und liefert einen aggregierten Snapshot.""" Refresh-Loops als Threads und liefert einen aggregierten Snapshot."""
@@ -555,6 +589,9 @@ class TradingEngine:
# dessen 2×ATR-SL allein schon >pct% verlieren kann). 0 = aus. # dessen 2×ATR-SL allein schon >pct% verlieren kann). 0 = aus.
try: try:
self._cb_limit_pct = max(0.0, float(self.cfg["trading"].get("daily_loss_limit_pct", "0") or 0)) self._cb_limit_pct = max(0.0, float(self.cfg["trading"].get("daily_loss_limit_pct", "0") or 0))
# Toggle-Gedaechtnis: ohne das waere die Prozentzahl nach
# einmal Ausschalten verloren (s. `set_circuit_breaker`).
self._cb_remember = self._cb_limit_pct or 8.0
except (TypeError, ValueError): except (TypeError, ValueError):
self._cb_limit_pct = 0.0 self._cb_limit_pct = 0.0
self._circuit_tripped_date = None # Berlin-Datum, an dem der Breaker griff self._circuit_tripped_date = None # Berlin-Datum, an dem der Breaker griff
@@ -1024,7 +1061,10 @@ class TradingEngine:
if err: if err:
log.error(f"{ok_word} fehlgeschlagen: {err}") log.error(f"{ok_word} fehlgeschlagen: {err}")
if tg_on: if tg_on:
try: send_telegram(f"⚠ <b>{ok_word} FEHLGESCHLAGEN</b>\n{sym} P&L {pnl:+.2f} " # BYPASS am Gate vorbei: die Position ist hier NICHT geschlossen und
# es braucht eine Handeingabe. Stille waere hier der
# gefaehrlichste Fall.
try: _send_telegram_roh(f"⚠ <b>{ok_word} FEHLGESCHLAGEN</b>\n{sym} P&L {pnl:+.2f} "
f"({level_txt}) — bitte MANUELL schließen! ({err})", f"({level_txt}) — bitte MANUELL schließen! ({err})",
tg["bot_token"], tg["chat_id"]) tg["bot_token"], tg["chat_id"])
except Exception as e: log.warning(f"Auto-Close-Telegram (Fehler): {e}") except Exception as e: log.warning(f"Auto-Close-Telegram (Fehler): {e}")
@@ -1453,6 +1493,33 @@ class TradingEngine:
self._save_runtime_state() self._save_runtime_state()
return self._auto_squeeze return self._auto_squeeze
def set_circuit_breaker(self, on: bool) -> bool:
"""Tagesverlust-Stopp an/aus (User-Wunsch 2026-08-19, Dashboard-Schalter).
Der Breaker hat KEINEN eigenen An/Aus-Schalter, sondern lebt in
`_cb_limit_pct` (0 = aus). Ein Toggle muss die Prozentzahl also merken,
sonst waere sie nach einmal Ausschalten fuer immer weg. `_cb_remember`
haelt den letzten Wert > 0; Startwert ist die ini (`daily_loss_limit_pct`,
aktuell 8), Rueckfall 8, falls die ini je auf 0 stand.
WAS ER TUT UND WAS NICHT: er SCHLIESST die offene Position bei
Erreichen des Tageslimits (das wirkt auf ALLE Trades) und sperrt neue
AUTONOME Entries bis zum naechsten Tag (`_squeeze_guard`). Manuelle
Orders sperrt er NICHT gemessen sind nur 6,1 % der Trades autonom,
die schliessende Haelfte ist also die wirksame. Real ausgeloest am
17.08. 18:35 (-82,42 <= -81,45 EUR).
"""
if on:
self._cb_limit_pct = float(getattr(self, "_cb_remember", 0.0) or 8.0)
else:
if self._cb_limit_pct > 0:
self._cb_remember = self._cb_limit_pct
self._cb_limit_pct = 0.0
log.info(f"Circuit-Breaker {'AN' if self._cb_limit_pct > 0 else 'AUS'}"
f" ({self._cb_limit_pct:.0f} %)")
self._save_runtime_state()
return self._cb_limit_pct > 0
def _check_adverse15(self): def _check_adverse15(self):
"""15-Minuten-Regel (User-Wunsch 2026-07-30): steht ein **vom Bot eröffneter** """15-Minuten-Regel (User-Wunsch 2026-07-30): steht ein **vom Bot eröffneter**
Trade 15 min nach Entry mehr als `adverse_15min_atr`×ATR im Minus schließen Trade 15 min nach Entry mehr als `adverse_15min_atr`×ATR im Minus schließen
@@ -2094,6 +2161,10 @@ class TradingEngine:
if "manual_margin" in st: if "manual_margin" in st:
from core.config import set_manual_margin as _set from core.config import set_manual_margin as _set
self._manual_margin = _set(st["manual_margin"] or 0) self._manual_margin = _set(st["manual_margin"] or 0)
if "cb_remember" in st:
self._cb_remember = float(st["cb_remember"] or 8.0)
if "cb_limit_pct" in st:
self._cb_limit_pct = max(0.0, float(st["cb_limit_pct"] or 0))
if "margin_pct" in st: if "margin_pct" in st:
from core.config import set_margin_buffer as _setp, get_margin_buffer from core.config import set_margin_buffer as _setp, get_margin_buffer
_setp(float(st["margin_pct"] or 0) or 95.0) _setp(float(st["margin_pct"] or 0) or 95.0)
@@ -2125,6 +2196,10 @@ class TradingEngine:
"sr_close_min_gain": self._sr_close_min_gain, "sr_close_min_gain": self._sr_close_min_gain,
"manual_margin": getattr(self, "_manual_margin", 0.0), "manual_margin": getattr(self, "_manual_margin", 0.0),
"margin_pct": getattr(self, "_margin_pct", 0.0), "margin_pct": getattr(self, "_margin_pct", 0.0),
# Circuit-Breaker als PROZENTZAHL (nicht bool):
# 0 = aus. So ueberlebt auch die Hoehe den Neustart.
"cb_limit_pct": getattr(self, "_cb_limit_pct", 0.0),
"cb_remember": getattr(self, "_cb_remember", 8.0),
# Kegel-ACI-Skalen (2026-08-08): ohne Persistenz waere # Kegel-ACI-Skalen (2026-08-08): ohne Persistenz waere
# die muehsam erlernte Kalibrierung nach jedem Neustart # die muehsam erlernte Kalibrierung nach jedem Neustart
# weg und liefe von 1,0 neu an. ⚠ JSON-Schluessel sind # weg und liefe von 1,0 neu an. ⚠ JSON-Schluessel sind
File diff suppressed because it is too large Load Diff
+13
View File
@@ -353,6 +353,19 @@ async def toggle_srclose(x_auth_token: str | None = Header(default=None)):
return {"ok": True, "enabled": state} return {"ok": True, "enabled": state}
@app.post("/api/circuit")
async def toggle_circuit(x_auth_token: str | None = Header(default=None)):
"""Tagesverlust-Stopp (Circuit Breaker) an/aus (User-Wunsch 2026-08-19)."""
_require_auth(x_auth_token)
eng = app.state.engine
state = eng.set_circuit_breaker(not (eng._cb_limit_pct > 0))
# Herkunft mitloggen wie bei den anderen Schaltern -- sonst ist beim
# Nachvollziehen eines Zustandswechsels nicht feststellbar, ob Dashboard
# oder Code geschaltet hat (Fix-Muster vom 02.08.).
log.info(f"[WEB] Circuit-Breaker {'AN' if state else 'AUS'}")
return {"ok": True, "enabled": state, "limit_pct": eng._cb_limit_pct}
@app.post("/api/autosqueeze") @app.post("/api/autosqueeze")
async def toggle_autosqueeze(x_auth_token: str | None = Header(default=None)): async def toggle_autosqueeze(x_auth_token: str | None = Header(default=None)):
"""Autonomen Squeeze-Entry (echte Order auf Ausbruch) an/aus.""" """Autonomen Squeeze-Entry (echte Order auf Ausbruch) an/aus."""
+474
View File
@@ -0,0 +1,474 @@
#!/usr/bin/env python3
"""
server.py — Web-Backend für den Oil Trading Agent
===================================================
FastAPI-Server, der die headless `TradingEngine` (core/engine.py) fährt und
ihren Zustand per REST bereitstellt. Grundlage für die Mobile-Web-Oberfläche.
Läuft auf demselben Windows-Rechner wie das MT5-Terminal. Das Tkinter-Widget
ist NICHT nötig — Server und Widget teilen sich nur die core/-Logik. Betreibe
entweder das Widget ODER den Server gegen dasselbe MT5-Terminal.
Start:
pip install fastapi "uvicorn[standard]"
python server.py
→ http://localhost:8000/api/snapshot
Aktuell (Schritt 1): nur Read-only-Endpoints.
GET / → Status-Kurzinfo
GET /api/health → läuft die Engine, ist MT5 verbunden?
GET /api/snapshot → kompletter Live-Zustand (Markt/Position/Analyse/Agent)
"""
from __future__ import annotations
import time
import asyncio
import contextlib
import secrets
import sys
from pathlib import Path
try:
import uvicorn
from fastapi import (FastAPI, WebSocket, WebSocketDisconnect,
Header, HTTPException, Body)
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
except ImportError:
print('pip install fastapi "uvicorn[standard]"')
sys.exit(1)
from core.config import save_config
from core.engine import TradingEngine
from core.logger import get_logger
log = get_logger("server")
WEB_DIR = Path(__file__).parent / "web"
WS_PUSH_S = 1.0 # Push-Intervall des Live-Snapshots
# Im Heim-VPN (WireGuard) erreichen Handy/Frontend den Server direkt; CORS
# offen lassen ist hier vertretbar. Bei öffentlichem Betrieb einschränken.
HOST = "0.0.0.0"
PORT = 8000
def _ensure_token(engine: TradingEngine) -> str:
"""App-Token für Trade-Aktionen sicherstellen — bei Bedarf generieren."""
cfg = engine.cfg
if "web" not in cfg:
cfg["web"] = {}
token = cfg["web"].get("api_token", "").strip()
if not token:
token = secrets.token_urlsafe(16)
cfg["web"]["api_token"] = token
try:
save_config(cfg)
except Exception as e:
log.warning(f"Token speichern: {e}")
log.warning("=" * 54)
log.warning(f" APP-TOKEN (für Trade-Steuerung am Handy):\n {token}")
log.warning(" Einmalig im Handy-UI eingeben. Liegt in [web] api_token.")
log.warning("=" * 54)
return token
@contextlib.asynccontextmanager
async def lifespan(app: FastAPI):
engine = TradingEngine()
app.state.token = _ensure_token(engine)
if not engine.start():
log.error(f"Engine-Start fehlgeschlagen: {engine._error}")
# Server läuft trotzdem weiter, damit /api/health den Fehler zeigt.
app.state.engine = engine
try:
yield
finally:
engine.stop()
log.info("Engine gestoppt")
def _require_auth(x_auth_token: str | None):
"""Prüft den App-Token (konstante-Zeit-Vergleich).
Ist die Token-Pflicht abgeschaltet ([web] require_token=false), entfällt
die Prüfung — der Zugang ist dann anderweitig abzusichern (z.B. WireGuard)."""
if not getattr(app.state.engine, "require_token", True):
return
expected = app.state.token
if not x_auth_token or not secrets.compare_digest(x_auth_token, expected):
raise HTTPException(status_code=401, detail="Ungültiger Token")
app = FastAPI(title="Oil Trading Agent", version="1.0", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], allow_methods=["*"], allow_headers=["*"],
)
@app.middleware("http")
async def _no_cache(request, call_next):
"""UI-Dateien NIE cachen (no-store) — sonst läuft am Handy alte JS/HTML
weiter. no-store statt no-cache, damit der Browser jedesmal frisch lädt."""
resp = await call_next(request)
path = request.url.path
if path == "/" or path.endswith((".js", ".css", ".html", ".json")):
resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
resp.headers["Pragma"] = "no-cache"
resp.headers["Expires"] = "0"
return resp
@app.get("/api/health")
def health():
eng = app.state.engine
return {"running": eng._running,
"connected": eng._connected,
"error": eng._error,
"symbol": eng.data.symbol,
"wave_tf": eng._wave_tf_label}
@app.get("/api/snapshot")
def snapshot():
eng = app.state.engine
return JSONResponse(content=_json_safe(eng.snapshot_cached()))
@app.get("/api/logs")
def logs(lines: int = 200):
"""Letzte N Zeilen der Logdatei (nur das Dateiende lesen — effizient)."""
from core.logger import LOG_FILE
import os
lines = max(1, min(lines, 1000))
try:
size = os.path.getsize(LOG_FILE)
with open(LOG_FILE, "rb") as f:
f.seek(max(0, size - 256 * 1024)) # nur die letzten 256 KB
data = f.read().decode("utf-8", "ignore")
tail = data.splitlines()[-lines:]
return {"lines": tail}
except Exception as e:
return {"error": str(e), "lines": []}
def _add_net(stats: dict, rate: float, trader=None) -> dict:
"""Ergänzt eine stats_overview-Periode um Netto-Werte nach Quellensteuer.
⚠⚠ SEIT 2026-08-12 AUS DEN **ECHTEN** BUCHUNGEN, nicht mehr geschätzt.
Die alte Rechnung war `net_pnl = total_pnl gross_win × 26,375 %` und
unterstellte, die WHT sei endgültig verloren. Sie wird aber täglich per
`Tax settlement` erstattet — gemessen über 90 Tage zu **99 %**
(einbehalten 2.625,89 €, erstattet +2.608,83 €, verblieben 17,06 €).
Die Schätzung sagte 2.619,93 € und machte damit aus **+153 € rund
2.450 €**. ✅ Die Schätzung der EINBEHALTENEN Summe war fast exakt —
falsch war allein die Annahme, sie bleibe weg.
⚠ Fällt der Broker aus, wird auf die alte Schätzung zurückgefallen; das Feld
`wht_quelle` sagt, welche Zahl gerade drinsteht („echt" / „geschätzt").
⚠ KURZE ZEITRÄUME sind naturgemäss pessimistisch: die Erstattung kommt erst
am Folgetag. Für „today" fehlt sie also meist — deshalb `wht_lag`.
"""
if not stats or stats.get("error"):
return stats
gw = stats.get("gross_win", 0.0) or 0.0
gl = stats.get("gross_loss", 0.0) or 0.0
tp = stats.get("total_pnl", 0.0) or 0.0
echt = None
if trader is not None:
try:
echt = trader.steuer_buchungen(int(stats.get("since") or 0),
int(stats.get("until") or time.time()))
except Exception:
echt = None
if echt and echt.get("n"):
wht = -float(echt["netto"]) # netto ist negativ → Last positiv
stats["wht_quelle"] = "echt"
stats["wht_brutto"] = echt["wht"]
stats["wht_erstattet"] = echt["tax"]
stats["wht_lag"] = bool(echt["tax"] == 0 and echt["wht"] < 0)
else:
wht = gw * rate
stats["wht_quelle"] = "geschätzt"
stats["wht_pct"] = round(rate * 100, 3)
stats["wht"] = round(wht, 2)
stats["net_pnl"] = round(tp - wht, 2)
stats["net_profit_factor"] = ((gw - wht) / gl) if gl > 0 else None
stats["net_avg_win"] = (stats.get("avg_win", 0.0) or 0.0) * (
1 - (wht / gw if gw > 0 else 0.0))
return stats
def _add_net_setup(s: dict, rate: float) -> dict:
"""Netto-Werte nach WHT für eine Setup-Zeile (nur Gewinnseite besteuert)."""
gw = s.get("gross_win", 0.0) or 0.0
gl = s.get("gross_loss", 0.0) or 0.0
s["net_pnl"] = (s.get("total_pnl", 0.0) or 0.0) - gw * rate
s["net_profit_factor"] = (gw * (1 - rate) / gl) if gl > 0 else None
return s
@app.get("/api/stats")
def stats():
eng = app.state.engine
try:
rate = float(eng.cfg["trading"].get("wht_pct", "0")) / 100.0
except Exception:
rate = 0.0
out: dict = {"wht_pct": round(rate * 100, 3)}
for period in ("today", "week", "all"):
try:
out[period] = _add_net(eng.history.stats_overview(period), rate,
getattr(eng, "trader", None))
except Exception as e:
out[period] = {"error": str(e)}
try:
out["setups"] = [_add_net_setup(s, rate)
for s in eng.history.setup_stats("all")]
except Exception as e:
out["setups"] = [{"error": str(e)}]
try:
out["n_total"] = eng.history.n_total_trades()
except Exception:
out["n_total"] = None
return JSONResponse(content=_json_safe(out))
@app.get("/api/news")
def news():
eng = app.state.engine
headlines, last_fetch, error = eng.news.snapshot()
return JSONResponse(content=_json_safe({
"headlines": headlines[:30],
"last_fetch": last_fetch,
"error": error,
"sentiment": eng.news.sentiment_snapshot(),
}))
@app.get("/api/squeeze_monitor")
def squeeze_monitor():
"""Auto-Squeeze-B4-Monitor: Live-Trades (echt) + Mechanik (Entry-Edge isoliert)
gegen die Backtest-Erwartung. Read-only, kein Token. Cache-TTL 60 s im Engine."""
return JSONResponse(content=_json_safe(app.state.engine.squeeze_monitor()))
@app.get("/api/pbreak_accuracy")
def pbreak_accuracy(period: str = "all"):
"""Trefferquote der Abprall/Durchbruch-Prognose (P(break)-Modell), ausgewertet
gegen candles_m1. period: today|week|all. Read-only, kein Token."""
return JSONResponse(content=_json_safe(app.state.engine.history.pbreak_accuracy(period)))
@app.websocket("/ws")
async def ws_snapshot(websocket: WebSocket):
"""Pusht den Live-Snapshot im Sekundentakt. Mehrere Geräte parallel ok."""
await websocket.accept()
eng = app.state.engine
loop = asyncio.get_event_loop()
try:
while True:
# snapshot() ist blockierend (Locks + sqlite) → im Threadpool holen,
# damit der Event-Loop frei bleibt. Cache teilt EINE Berechnung über
# alle WS-Clients (TTL) → DB-Last unabhängig von der Geräteanzahl.
snap = await loop.run_in_executor(None, eng.snapshot_cached)
await websocket.send_json(_json_safe(snap))
await asyncio.sleep(WS_PUSH_S)
except WebSocketDisconnect:
pass
except Exception as e:
log.warning(f"WebSocket beendet: {e}")
# ── Trade-Steuerung (token-geschützt) ─────────────────────────────────────────
@app.post("/api/auth/check")
async def auth_check(x_auth_token: str | None = Header(default=None)):
"""Frontend prüft hiermit, ob der eingegebene Token gültig ist."""
_require_auth(x_auth_token)
return {"ok": True}
@app.post("/api/order")
async def order(payload: dict = Body(...),
x_auth_token: str | None = Header(default=None)):
_require_auth(x_auth_token)
side = (payload.get("side") or "").lower()
eng = app.state.engine
loop = asyncio.get_event_loop()
if side == "long":
fn = eng.open_long
elif side == "short":
fn = eng.open_short
elif side == "close":
fn = eng.close
else:
raise HTTPException(status_code=400, detail="side muss long|short|close sein")
log.info(f"[WEB] Order angefordert: {side}")
err = await loop.run_in_executor(None, fn) # blockierender MT5-Call
if err:
log.warning(f"[WEB] Order {side} fehlgeschlagen: {err}")
return JSONResponse(status_code=200, content={"ok": False, "error": str(err)})
return {"ok": True, "side": side}
@app.post("/api/emergency")
async def emergency(payload: dict = Body(...),
x_auth_token: str | None = Header(default=None)):
"""Notfall-Verlust-Schwelle setzen/löschen. `value`: Betrag (Kontowährung)
oder null/0 = aus. Der Server schließt die Position, sobald P&L ≤ value."""
_require_auth(x_auth_token)
cur = app.state.engine.set_emergency(payload.get("value"))
log.info(f"[WEB] Notfall-Stop gesetzt: {cur}")
return {"ok": True, "emergency_loss": cur}
@app.post("/api/takeprofit")
async def takeprofit(payload: dict = Body(...),
x_auth_token: str | None = Header(default=None)):
"""Gewinn-Ziel setzen/löschen. `value`: Betrag (Kontowährung) oder null/0 = aus.
Der Server schließt die Position, sobald P&L ≥ +value."""
_require_auth(x_auth_token)
cur = app.state.engine.set_takeprofit(payload.get("value"))
log.info(f"[WEB] Gewinn-Ziel gesetzt: {cur}")
return {"ok": True, "takeprofit": cur}
@app.post("/api/sltp")
async def set_sltp(payload: dict = Body(...),
x_auth_token: str | None = Header(default=None)):
"""Manuelles SL/TP der offenen Position setzen (deaktiviert das Trailing).
`sl`/`tp`: Preis oder null (= unverändert). Broker-Prüfung → error bei Ablehnung."""
_require_auth(x_auth_token)
res = await asyncio.get_event_loop().run_in_executor(
None, lambda: app.state.engine.set_sltp(payload.get("sl"), payload.get("tp")))
log.info(f"[WEB] SL/TP: {res}")
return res
@app.post("/api/srclose")
async def toggle_srclose(x_auth_token: str | None = Header(default=None)):
"""Automatischen S/R-Close (P(break)-Regel) an/aus."""
_require_auth(x_auth_token)
eng = app.state.engine
state = eng.set_sr_autoclose(not eng._auto_sr_close)
return {"ok": True, "enabled": state}
@app.post("/api/autosqueeze")
async def toggle_autosqueeze(x_auth_token: str | None = Header(default=None)):
"""Autonomen Squeeze-Entry (echte Order auf Ausbruch) an/aus."""
_require_auth(x_auth_token)
eng = app.state.engine
state = eng.set_auto_squeeze(not eng._auto_squeeze)
# ⚠ Herkunft mitloggen (Fix 2026-08-02, Review-Durchgang 1): `set_auto_squeeze`
# loggt nur „Auto-Squeeze-Entry AN/AUS" ohne Quelle — beim Nachvollziehen eines
# Zustandswechsels war dadurch nicht feststellbar, ob der Dashboard-Button oder
# Code dahinterstand (real: BRK stand auf AUS und es kostete mehrere Schritte,
# das als Nutzeraktion zu belegen). `/api/autosignal` macht das längst richtig —
# bei einem Schalter für autonome ECHTGELD-Einstiege gehört die Herkunft ins Log.
get_logger("server").info(f"[WEB] Auto-Squeeze-Entry {'AN' if state else 'AUS'}")
return {"ok": True, "enabled": state}
@app.post("/api/manualmargin")
async def set_manual_margin(body: dict,
x_auth_token: str | None = Header(default=None)):
"""Feste Einsatz-Margin in Kontowährung setzen (User-Wunsch 2026-08-04).
`{"value": 0}` = automatisch (wie bisher `margin_buffer_pct` % der freien Margin).
⚠ Wirkt auf ALLE neuen Positionen — auch die autonomen. Die freie Margin bleibt
Obergrenze; `calc_lots` deckelt einen zu hohen Wunschwert und loggt das."""
_require_auth(x_auth_token)
try:
v = float(body.get("value") or 0)
except (TypeError, ValueError):
return {"ok": False, "error": "ungültiger Wert"}
eng = app.state.engine
val = eng.set_manual_margin(v)
get_logger("server").info(
f"[WEB] Einsatz-Margin: {'automatisch' if val <= 0 else f'{val:.2f}'}")
return {"ok": True, "value": val}
@app.post("/api/marginpct")
async def set_margin_pct(body: dict,
x_auth_token: str | None = Header(default=None)):
"""Einsatz als PROZENT der freien Margin setzen (User-Wunsch 2026-08-05).
Gegenstück zu `/api/manualmargin` (fester Betrag). ⚠ Ein gesetzter FESTER
Betrag hat Vorrang; der Prozentsatz bleibt dann nur noch die Obergrenze.
Wird auf 199 % geklemmt — 100 % ließe keinen Puffer für Spread/Swap."""
_require_auth(x_auth_token)
try:
v = float(body.get("value") or 0)
except (TypeError, ValueError):
return {"ok": False, "error": "ungültiger Wert"}
if v <= 0:
return {"ok": False, "error": "Prozentsatz muss > 0 sein"}
val = app.state.engine.set_margin_pct(v)
get_logger("server").info(f"[WEB] Einsatz-Prozentsatz: {val:.0f} % der freien Margin")
return {"ok": True, "value": val}
# ⚠⚠ `/api/autosignal` ENTFERNT am 2026-08-12 (User-Entscheidung). Der autonome
# Entry auf die Empfehlung war seit dem 01.08. aus und auf BEIDEN
# Ausführungs-Achsen gemessen negativ — Markt-Einstieg in allen 8 Feldern,
# ruhende Order mit Touch-Fill in allen 6 und dabei sogar schlechter als
# Markt. Ursache strukturell: das Bestätigungs-Level wandert.
# Sicherung: `.removed_backup/auto_signal_2026-08-12.py` + Git-Historie.
@app.post("/api/srclose_min")
async def srclose_min(payload: dict = Body(...),
x_auth_token: str | None = Header(default=None)):
"""Mindestgewinn (EUR) für den S/R-Auto-Close setzen. 0/leer = aus."""
_require_auth(x_auth_token)
get_logger("server").info(f"[WEB] Mindestgewinn angefordert: {payload.get('value')!r}")
cur = app.state.engine.set_sr_close_min_gain(payload.get("value"))
return {"ok": True, "min_gain": cur}
@app.post("/api/trail")
async def toggle_trail(x_auth_token: str | None = Header(default=None)):
_require_auth(x_auth_token)
state = await asyncio.get_event_loop().run_in_executor(
None, app.state.engine.toggle_trail)
return {"ok": True, "enabled": state}
def _json_safe(obj):
"""Macht den Snapshot robust JSON-fähig (numpy-Floats, datetime, Tupel)."""
import datetime as _dt
if isinstance(obj, dict):
return {str(k): _json_safe(v) for k, v in obj.items()}
if isinstance(obj, (list, tuple)):
return [_json_safe(v) for v in obj]
if isinstance(obj, _dt.datetime):
return obj.isoformat()
# numpy-Skalare → Python-Zahl
if hasattr(obj, "item") and not isinstance(obj, (str, bytes)):
try:
return obj.item()
except Exception:
return str(obj)
return obj
# Frontend zuletzt mounten, damit /api/* und /ws Vorrang haben.
# html=True liefert web/index.html unter "/" aus.
if WEB_DIR.is_dir():
app.mount("/", StaticFiles(directory=str(WEB_DIR), html=True), name="web")
else:
log.warning(f"Web-Verzeichnis fehlt: {WEB_DIR}")
if __name__ == "__main__":
print("=" * 58)
print(" Oil Trading Agent — Web-Backend (FastAPI)")
print(f" Mobile-UI: http://localhost:{PORT}/")
print(f" JSON-API: http://localhost:{PORT}/api/snapshot")
print("=" * 58)
# access_log=False: Uvicorns Zugriffs-Log (eine Zeile je HTTP-Request) schweigt
# jetzt — seit dem 4-s-REST-Polling (2026-07-22) + WS-Push flutete das die Konsole
# (GET /api/snapshot alle paar Sekunden). Betrifft NUR das Access-Log, nicht die
# eigenen oil.*-Logger (core/logger.py, eigene Hierarchie) oder Uvicorn-Fehler.
uvicorn.run(app, host=HOST, port=PORT, log_level="info", access_log=False)
+17 -95
View File
@@ -1144,7 +1144,18 @@ function render(d) {
longBtn.disabled = hasPos; longBtn.disabled = hasPos;
shortBtn.disabled = hasPos; shortBtn.disabled = hasPos;
closeBtn.disabled = !hasPos; closeBtn.disabled = !hasPos;
$("btn-squeeze").classList.toggle("on", !!d.auto_squeeze); // BRK-Schalter ENTFERNT (User 2026-08-19), dafuer der Circuit-Breaker. Der
// Auto-Squeeze-ZUSTAND bleibt in der Breakout-Karte sichtbar -- nur sein
// Schalter ist weg (jetzt POST-Endpunkt bzw. ini).
const cb = d.circuit_breaker || {};
const cbb = $streng("btn-cb");
if (cbb) {
cbb.classList.toggle("on", !!cb.enabled);
cbb.title = cb.enabled
? "Tagesverlust-Stopp AN: schliesst bei " + fmt(cb.limit_eur, 2) + " € ("
+ cb.limit_pct + " % der Balance). Heute: " + fmt(cb.day_total, 2) + " €."
: "Tagesverlust-Stopp AUS — kein automatischer Schutz gegen eine Verlust-Serie.";
}
// ⚠⚠ Der 🎯 SIG-Button ist am 2026-08-12 ENTFERNT (User-Entscheidung). Der // ⚠⚠ Der 🎯 SIG-Button ist am 2026-08-12 ENTFERNT (User-Entscheidung). Der
// autonome Entry auf die Empfehlung war seit dem 01.08. aus und auf BEIDEN // autonome Entry auf die Empfehlung war seit dem 01.08. aus und auf BEIDEN
// Ausführungs-Achsen gemessen negativ — Markt in allen 8 Feldern, ruhende // Ausführungs-Achsen gemessen negativ — Markt in allen 8 Feldern, ruhende
@@ -1306,32 +1317,6 @@ function render(d) {
if (smi && document.activeElement !== smi) { if (smi && document.activeElement !== smi) {
smi.value = srMin > 0 ? String(srMin) : ""; smi.value = srMin > 0 ? String(srMin) : "";
} }
// Feste Einsatz-Margin (0/leer = automatisch). Nicht überschreiben, während
// getippt wird — sonst löscht der 1-s-Render die Eingabe unter den Fingern weg.
const mgi = $("margin-input");
if (mgi && document.activeElement !== mgi) {
const mm = d.manual_margin || 0;
mgi.value = mm > 0 ? String(mm) : "";
mgi.classList.toggle("on", mm > 0);
}
// Einsatz in Prozent der freien Margin. Ausgegraut, sobald links ein fester
// Betrag steht — der hat Vorrang, und ohne den Hinweis wäre unklar, was gilt.
const mpi = $("marginpct-input");
if (mpi && document.activeElement !== mpi) {
const mp = d.margin_pct || 0;
mpi.value = mp > 0 ? String(mp) : "";
const fest = (d.manual_margin || 0) > 0;
mpi.classList.toggle("on", mp > 0 && !fest);
const wrap = $("marginpct-wrap");
if (wrap) {
wrap.classList.toggle("muted", fest);
wrap.title = fest
? `Ohne Wirkung: links steht ein fester Einsatz (${fmt(d.manual_margin, 2)} €). `
+ `Der Prozentsatz (${mp} %) bleibt nur die Obergrenze.`
: "Einsatz als Prozent der freien Margin (199). "
+ "Greift, solange links kein fester Betrag steht.";
}
}
// Einblendung: der Bot hat am S/R automatisch geschlossen (Zähler stieg) // Einblendung: der Bot hat am S/R automatisch geschlossen (Zähler stieg)
const src = d.sr_close_count; const src = d.sr_close_count;
if (src != null) { if (src != null) {
@@ -1744,7 +1729,7 @@ $("mute-btn").onclick = () => {
_updateMuteBtn(); _updateMuteBtn();
// Toggle-Buttons (AUTO/TRAIL/S-R-Close) — ohne Bestätigung // Toggle-Buttons (AUTO/TRAIL/S-R-Close) — ohne Bestätigung
$("btn-squeeze").onclick = () => toggle("api/autosqueeze", "Auto-Squeeze (autonom!)"); $("btn-cb").onclick = () => toggle("api/circuit", "Tagesverlust-Stopp");
// (🎯 SIG entfernt 2026-08-12 — s. renderOrderButtons oben) // (🎯 SIG entfernt 2026-08-12 — s. renderOrderButtons oben)
$("btn-trail").onclick = () => toggle("api/trail", "Trailing"); $("btn-trail").onclick = () => toggle("api/trail", "Trailing");
$("srclose-btn").onclick = () => toggle("api/srclose", "Auto-S/R-Close"); $("srclose-btn").onclick = () => toggle("api/srclose", "Auto-S/R-Close");
@@ -1776,73 +1761,10 @@ $("srmin-input").addEventListener("keydown", e => {
if (e.key === "Enter") { e.preventDefault(); e.target.blur(); } if (e.key === "Enter") { e.preventDefault(); e.target.blur(); }
}); });
// Feste Einsatz-Margin (User-Wunsch 2026-08-04). Leer/0 = automatisch. // Einsatz-Felder ENTFERNT (User 2026-08-19). Das BACKEND bleibt vollstaendig:
// ⚠ Wirkt auf ALLE neuen Positionen, auch die autonomen — deshalb sagt der Toast // beide Endpunkte sind weiter erreichbar, die Werte leben in
// ausdrücklich, was gilt, statt nur „gespeichert". // `runtime_state.json` UND der ini (aktuell 40 %). Ohne UI aendert man den
async function setManualMargin() { // Prozentsatz also dort + Neustart -- oder per POST.
const el = $("margin-input"); if (!el) return;
const raw = el.value;
const v = raw === "" ? 0 : parseFloat(raw);
if (isNaN(v) || v < 0) { toast("Bitte Betrag ≥ 0 eingeben (0 = automatisch)", false); return; }
const tok = await ensureToken();
if (tok === null) return;
el.blur();
try {
const r = await fetch("api/manualmargin", { method: "POST",
headers: { "X-Auth-Token": tok, "Content-Type": "application/json" },
body: JSON.stringify({ value: v }) });
if (r.status === 401) { clearToken(); throw new Error("401"); }
const j = await r.json();
toast(v > 0 ? `Einsatz fest: ${fmt(j.value ?? v, 2)} € je Position`
: "Einsatz wieder automatisch (95 % der freien Margin)", true);
} catch (e) {
toast(e.message === "401" ? "Token abgelaufen — neu eingeben" : "Netzwerkfehler", false);
}
}
(() => {
const el = $("margin-input");
if (!el) return; // null-sicher (Cache-Regel)
el.addEventListener("change", setManualMargin);
el.addEventListener("keydown", e => {
if (e.key === "Enter") { e.preventDefault(); e.target.blur(); }
});
})();
// Einsatz in PROZENT der freien Margin (User-Wunsch 2026-08-05) — das Gegenstück
// zum festen Betrag oben. Steuert `margin_buffer_pct`, das bisher nur in der ini
// stand. ⚠ Ein gesetzter fester Betrag hat Vorrang; darauf weist der Toast hin.
async function setMarginPct() {
const el = $("marginpct-input"); if (!el) return;
const v = parseFloat(el.value);
if (isNaN(v) || v < 1 || v > 99) {
toast("Bitte 199 % eingeben (100 % ließe keinen Puffer für Spread/Swap)", false);
return;
}
const tok = await ensureToken();
if (tok === null) return;
el.blur();
try {
const r = await fetch("api/marginpct", { method: "POST",
headers: { "X-Auth-Token": tok, "Content-Type": "application/json" },
body: JSON.stringify({ value: v }) });
if (r.status === 401) { clearToken(); throw new Error("401"); }
const j = await r.json();
if (j.ok === false) { toast(j.error || "abgelehnt", false); return; }
const fest = parseFloat(($("margin-input") || {}).value || "0") > 0;
toast(`Einsatz: ${fmt(j.value ?? v, 0)} % der freien Margin`
+ (fest ? " — greift erst, wenn der feste Betrag links leer ist" : ""), true);
} catch (e) {
toast(e.message === "401" ? "Token abgelaufen — neu eingeben" : "Netzwerkfehler", false);
}
}
(() => {
const el = $("marginpct-input");
if (!el) return; // null-sicher (Cache-Regel)
el.addEventListener("change", setMarginPct);
el.addEventListener("keydown", e => {
if (e.key === "Enter") { e.preventDefault(); e.target.blur(); }
});
})();
// Notfall-Stop: manueller Verlust-Wert, bei dem der Server sofort schließt. // Notfall-Stop: manueller Verlust-Wert, bei dem der Server sofort schließt.
// Wieder eingeblendet 2026-07-22 (Backend/Persistenz waren die ganze Zeit aktiv). // Wieder eingeblendet 2026-07-22 (Backend/Persistenz waren die ganze Zeit aktiv).
+8 -26
View File
@@ -12,7 +12,7 @@
⚠ LOKAL eingebunden, kein CDN: die PWA laeuft ueber WireGuard ⚠ LOKAL eingebunden, kein CDN: die PWA laeuft ueber WireGuard
und muss offline funktionieren. --> und muss offline funktionieren. -->
<link rel="stylesheet" href="pico.min.css?v=1"> <link rel="stylesheet" href="pico.min.css?v=1">
<link rel="stylesheet" href="style.css?v=176"> <link rel="stylesheet" href="style.css?v=177">
</head> </head>
<body> <body>
<!-- Modul-Menü (ganz oben) --> <!-- Modul-Menü (ganz oben) -->
@@ -339,33 +339,15 @@
<!-- Aktionsleiste (Trade-Steuerung) --> <!-- Aktionsleiste (Trade-Steuerung) -->
<nav id="actions"> <nav id="actions">
<!-- Einsatz-Margin manuell vorgeben (User 2026-08-04). Leer/0 = automatisch <!-- Einsatz-Felder ENTFERNT (User 2026-08-19). Der Einsatz steht auf
(margin_buffer_pct % der freien Margin). ⚠ BEWUSST hier und nicht in der 40 % (ini + `runtime_state.json`); ohne UI aendert man ihn dort
Trade-Leiste: die ist nur bei OFFENER Position sichtbar, der Einsatz muss + Neustart. -->
aber VOR dem Öffnen einstellbar sein. Wirkt auf ALLE neuen Positionen. -->
<span class="mg-field" title="Feste Einsatz-Margin in € — leer/0 = automatisch (dann gilt der Prozentsatz rechts). Gilt auch für autonome Trades.">
<label for="margin-input">Einsatz €</label>
<input id="margin-input" type="number" inputmode="decimal" step="10" min="0" placeholder="auto">
</span>
<!-- Prozent-Gegenstück (User 2026-08-05). Steuert `margin_buffer_pct`, das
bisher nur in der ini stand. ⚠ Der feste €-Betrag hat VORRANG — steht er,
wird dieses Feld ausgegraut, damit nicht unklar bleibt, was gilt. -->
<span class="mg-field" id="marginpct-wrap" title="Einsatz als Prozent der freien Margin (199). Greift, solange links kein fester Betrag steht.">
<label for="marginpct-input">Einsatz %</label>
<input id="marginpct-input" type="number" inputmode="decimal" step="5" min="1" max="99" placeholder="95">
</span>
<!-- Raum zum naechsten Level IN Trade-Richtung, direkt auf dem Knopf, den
es betrifft (2026-08-19). ⚠ Gemessen auf 27 EIGENEN Trades: Raum
< 0,6×ATR = 79 % Trefferquote und trotzdem 7,57 bis 11,51 €/Lot
(Klein-Close-Falle — Gewinn am Level gedeckelt, Verlust laeuft bis zum
Stop; ueber 80k Bars als PF 0,42/0,66 belegt). Die Zahl stand bisher
nur in Gate ③b der M15-Karte — nicht dort, wo geklickt wird.
⚠ REINE ANZEIGE: kein Block, keine Sperre, kein Dialog. User-
Uebersteuerungen laufen gemessen 68 % WR. -->
<button class="btn long" data-side="long">▲ LONG<span class="btn-raum" id="raum-long"></span></button> <button class="btn long" data-side="long">▲ LONG<span class="btn-raum" id="raum-long"></span></button>
<button class="btn short" data-side="short">▼ SHORT<span class="btn-raum" id="raum-short"></span></button> <button class="btn short" data-side="short">▼ SHORT<span class="btn-raum" id="raum-short"></span></button>
<button class="btn close" data-side="close">✕ CLOSE</button> <button class="btn close" data-side="close">✕ CLOSE</button>
<button class="btn tog" id="btn-squeeze" title="Autonomer Entry auf Squeeze-Ausbruch">🚀 BRK</button> <!-- BRK-Schalter ENTFERNT, dafuer der Circuit-Breaker (User 2026-08-19).
Der Auto-Squeeze-Zustand bleibt in der Breakout-Karte sichtbar. -->
<button class="btn tog" id="btn-cb" title="Tagesverlust-Stopp (Circuit Breaker) an/aus">🛑 STOPP</button>
<!-- 🎯 SIG (Auto-Signal-Entry) ENTFERNT 2026-08-12: seit 01.08. aus und auf <!-- 🎯 SIG (Auto-Signal-Entry) ENTFERNT 2026-08-12: seit 01.08. aus und auf
BEIDEN Ausfuehrungs-Achsen gemessen negativ (Markt alle 8 Felder, BEIDEN Ausfuehrungs-Achsen gemessen negativ (Markt alle 8 Felder,
ruhende Order mit Touch-Fill alle 6 und schlechter als Markt). --> ruhende Order mit Touch-Fill alle 6 und schlechter als Markt). -->
@@ -387,6 +369,6 @@
</div> </div>
<div id="toast" class="toast hidden"></div> <div id="toast" class="toast hidden"></div>
<script src="app.js?v=176"></script> <script src="app.js?v=177"></script>
</body> </body>
</html> </html>
+392
View File
@@ -0,0 +1,392 @@
<!DOCTYPE html>
<html lang="de" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#0d1117">
<title>Oil · MT5</title>
<link rel="manifest" href="manifest.json">
<!-- ⚠ Pico ZUERST: es liefert die Basis (Typografie, Formulare,
Buttons, Fokus-Ringe). `style.css` kommt DANACH und gewinnt bei
Konflikten — die ~60 Trading-Klassen bleiben unveraendert.
⚠ LOKAL eingebunden, kein CDN: die PWA laeuft ueber WireGuard
und muss offline funktionieren. -->
<link rel="stylesheet" href="pico.min.css?v=1">
<link rel="stylesheet" href="style.css?v=176">
</head>
<body>
<!-- Modul-Menü (ganz oben) -->
<nav id="tabs">
<button class="tab active" data-view="dash">Dashboard</button>
<!-- Aufteilung nach WIRKUNG (User-Vorgabe 2026-08-05), nicht nach Thema:
„SIG Live" = Module MIT Verdict-Gewicht (fließen in die Empfehlung ein),
„SIG Anzeige" = gemessen OHNE Einfluss. Die Trennung ist am Code prüfbar
(`_verdict`: `add(...)` mit Gewicht > 0), nicht Geschmackssache. -->
<button class="tab" data-view="siglive">SIG Live</button>
<button class="tab" data-view="siganzeige">SIG Anzeige</button>
<button class="tab" data-view="logs">Logs</button>
<button class="tab" data-view="stats">Statistik</button>
<button class="tab" data-view="news">News</button>
<button class="tab mute-tab" id="mute-btn" title="Töne an/aus">🔊</button>
</nav>
<header id="top">
<div class="sym-row">
<span id="symbol">—</span>
<div class="hdr-right">
<div class="hdr-meta">
<span id="hdr-clock" class="hdr-clock">—</span>
<span id="session" class="session-line dim">—</span>
</div>
<span id="conn" class="dot off" title="Verbindung"></span>
</div>
</div>
<div class="price-row">
<div class="px"><label title="Kontostand ohne die offene Position.">BALANCE</label><span id="balance">—</span></div>
<div class="px"><label title="Realisiertes Ergebnis von heute. Brutto — die Quellensteuer wird größtenteils erstattet und ist hier nicht abgezogen.">TAG P/L</label><span id="day-pl">—</span></div>
<div class="px"><label title="Kontostand einschließlich der offenen Position.">EQUITY</label><span id="equity">—</span></div>
<div class="px"><label id="hdr-price-lbl" title="Der Kurs, zu dem tatsächlich gehandelt wird. Steht er länger als 3 Minuten still, gilt der Markt als geschlossen und das Label zeigt ZU.">PEPPERSTONE KURS</label><span id="hdr-price">—</span></div>
<!-- Hyperliquid-Kurs NEBEN dem Broker-Kurs (User 2026-08-01). HL handelt 24/7,
Pepperstone nicht — am Wochenende der einzige lebende Kurs. Basis-korrigiert,
also direkt vergleichbar. Beide Quellen AUSGESCHRIEBEN (User 2026-08-01):
„KURS"/„HL" ließ offen, welcher Kurs woher kommt. -->
<div class="px" id="px-hl"><label title="Roher Kurs des Hyperliquid-Öl-Perps — läuft 24/7, auch wenn Pepperstone geschlossen ist. Anderer Kontrakt, deshalb nicht identisch; der Tooltip am Wert nennt die umgerechnete Entsprechung.">HYPERLIQUID KURS</label><span id="hdr-hl">—</span></div>
<div class="px"><label title="Gewinn/Verlust der offenen Position, NETTO nach Quellensteuer. Bei geschlossenem Markt eine Schätzung auf Basis des Hyperliquid-Kurses (mit ≈ markiert).">G/V</label><span id="hdr-pnl">—</span></div>
</div>
</header>
<main id="view-dash" class="view">
<!-- Circuit Breaker (Tagesverlust-Stopp) — roter Banner, nur wenn ausgelöst/nah -->
<div id="circuit-banner" class="circuit hidden"></div>
<!-- Aktueller Trade (nur bei offener Position) — Anzeige + SL/TP/Close editierbar -->
<section class="card wide trade-bar hidden" id="trade-bar">
<h2 class="tb-title" title="Der aktuell offene Trade. SL, TP und die Close-Schwellen sind hier direkt editierbar — Enter oder Verlassen des Feldes sendet.">aktueller Trade läuft <span class="tb-runtime" id="tb-runtime"></span></h2>
<div class="tb-row">
<span class="tb-field"><label title="Wie lange der Trade schon läuft. Ab 2 Stunden bernstein — dann greift der Time-Stop, falls der Trade nie in die Gewinnphase kam.">Laufzeit</label>
<b class="tb-val" id="tb-dur">—</b></span>
<span class="tb-field"><label title="LONG = auf steigende Kurse, SHORT = auf fallende.">Richtung</label>
<b class="tb-val tb-dir" id="tb-dir">—</b></span>
<span class="tb-field"><label title="Positionsgröße. Ergibt sich aus dem Einsatz-Feld in der Order-Leiste.">Lots</label>
<b class="tb-val" id="tb-lots">—</b></span>
<span class="tb-field"><label title="Die gebundene Margin dieser Position.">Einsatz</label>
<b class="tb-val" id="tb-margin">—</b></span>
<span class="tb-field"><label title="Schließt am nächsten S/R-Level automatisch, wenn das Level wahrscheinlich hält. Derzeit AUS.">Auto-Close</label>
<button id="srclose-btn" class="tb-val tb-btn">—</button></span>
<span class="tb-field stop"><label title="Stop-Loss beim Broker. Ändern schaltet das Trailing ab, damit es die Handeingabe nicht zurückzieht.">SL</label>
<input id="tb-sl" type="number" inputmode="decimal" step="0.01" placeholder="—"></span>
<span class="tb-field gain"><label title="Take-Profit beim Broker. Ändern schaltet das Trailing ab.">TP</label>
<input id="tb-tp" type="number" inputmode="decimal" step="0.01" placeholder="—"></span>
<span class="tb-field gain"><label title="Mindestgewinn in Euro, unter dem der Auto-Close am Level NICHT schließt. Leer oder 0 = aus.">Gewinn-Close</label>
<input id="srmin-input" type="number" inputmode="decimal" step="1" placeholder="€"></span>
<span class="tb-field stop"><label title="Harte Verlustgrenze in Euro — der Server schließt sofort, wenn sie erreicht ist. Wirkt auch bei gesperrtem Handy. ⚠ Ein gesetzter Wert wird auf den nächsten Trade übernommen, bis er gelöscht wird.">Notfall-Close</label>
<input id="emg-input" type="number" inputmode="decimal" step="1" placeholder="€"></span>
</div>
</section>
<!-- Gesamtempfehlung (aus allen Modulen aggregiert) -->
<!-- ══════════════════════════════════════════════════════════════════
GESAMTEMPFEHLUNG ENTFERNT (2026-08-08, User: „funktioniert so leider
nicht, entferne es komplett - wir bauen etwas Neues").
Weg sind: Headline, Konfidenz-Ring, Bias-Nadel, Modul-Chips,
Konsens-Zeile und die Block-Gruende. Die Aggregation war reine
Anzeige - die Order haengt an `wave_signal`, nicht am Verdict.
GEBLIEBEN sind drei Dinge, die dort nur geparkt waren und je eine
eigene, unabhaengige Beleglage haben (s. CLAUDE.md).
══════════════════════════════════════════════════════════════════ -->
<!-- ══════════════════════════════════════════════════════════════════
M15 SETUP-BEREITSCHAFT (2026-08-08) — Ersatz fuer die entfernte
Gesamtempfehlung. ⚠ KEINE Richtungsaussage: auf M15 traegt gemessen
KEINE (Squeeze n=33/Nachbarn kippen, VWAP-Baender ohne Edge, 24
Signal-Eingriffe gescheitert). Gezeigt werden BEDINGUNGEN nach dem
Schema Bias -> Zone -> Reichweite -> Veto; den Ausloeser gibt der
Mensch, dessen Auswahl-Vorteil unabhaengig belegt ist.
══════════════════════════════════════════════════════════════════ -->
<section class="card wide" id="card-m15">
<h2 title="Setup-Bereitschaft auf M15. Zeigt BEDINGUNGEN, keine Richtungsprognose - auf M15 traegt gemessen kein Richtungssignal. Der Ausloeser bleibt beim Menschen.">M15 · Setup-Bereitschaft</h2>
<!-- Status-Banner: die wichtigste Zeile. Sie bestaetigt ausdruecklich
auch das NICHT-Handeln - die gemessene Kern-Leckage sind
diskretionaere Abweichungen, nicht fehlende Gelegenheiten.
⚠ Die Order-Buttons werden BEWUSST NICHT gesperrt: hartes Blockieren
wurde im Projekt verworfen, und User-Uebersteuerungen liefen
gemessen 68 % WR. Sichtbar machen, nicht bevormunden. -->
<div class="m15-status" id="m15-status">—</div>
<div class="m15-grid">
<div class="m15-item" id="m15-bias" title="Uebergeordneter Trend M30 + H1. Der EINZIGE belegte Filter: gegen den HTF zu handeln halbiert den Edge (gemessen ×2).">—</div>
<div class="m15-item" id="m15-kosten" title="Spread geteilt durch ATR_M15. Auf M15 gemessen 0,104 gegen 0,193 auf M5 - hier ist die groebere Zeitebene echt im Vorteil. Schwellen 0,20 / 0,32 aus backtest_realcosts.py.">—</div>
<div class="m15-item" id="m15-zone" title="Naechstes STEHENDES Level (M30-Pivots, dieselbe Quelle wie Chart und S/R-Close) mit Abstand und Durchbruchwahrscheinlichkeit. ⚠ P(break) ist live derzeit rund 7 Pp zu niedrig kalibriert.">—</div>
<div class="m15-item" id="m15-reich" title="Erwartete Spanne aus core/cone.py - out-of-sample geprueft, die 80-%-Baender treffen real 77 %. Sagt WIE WEIT, nicht wohin.">—</div>
<!-- ③b RAUM + MOTOR-ZUSTAND (2026-08-10, nach Trade #49671601 / -87 EUR).
Die Karte zeigte GRUEN, waehrend der Signal-Motor zu 76 % mit
entry_room blockte: dieselbe Tatsache ("ein Level ist nah") wird von
Gate ③ als GUT und von entry_room als SCHLECHT gelesen - beide
gemessen, aber ueber verschiedene Trades. Diese Zeile schliesst die
Luecke. REINE ANZEIGE, blockt nichts. -->
<div class="m15-item" id="m15-motor" title="Was der Signal-Motor GERADE blockt, plus der Raum zum naechsten Gegenlevel je Richtung (M5, dieselbe Rechnung wie das Gate selbst). Unter 0,6xATR ist der Ertrag am Level gedeckelt - gemessen ØR -0,119/-0,067 bei PF 0,42/0,66, also Trefferquote hoch und trotzdem Verlust. ⚠ Gate ③ liest dieselbe Naehe als GUT (ruhende Order AM Level), entry_room als SCHLECHT (Markt-Einstieg mit dem Level als Hindernis). Beides stimmt - fuer verschiedene Trades.">—</div>
<!-- ⑥ SELBSTMESSUNG: die Karte zeigt die Trefferquote ihrer EIGENEN
Lesart. ⚠ Jeder Ausfall, der in diesem Projekt je aufgedeckt wurde,
kam aus einer Rueckkopplung — pbreak_predictions entlarvte den
Modellausfall LIVE, was kein Backtest konnte. -->
<div class="m15-item" id="m15-track" title="Trefferquote der Lesart dieser Karte, nach 30/60 Minuten gegen den echten Kurs ausgewertet. Gegen 50 % zu lesen, nicht gegen 0. Geloggt wird EIN Zustand je Wechsel, nicht je Tick - sonst zaehlt man dieselbe Marktlage dutzendfach.">—</div>
<!-- ⑦ AUSFUEHRUNGSQUALITAET: der einzige gemessen grosse Hebel (+0,32 R). -->
<div class="m15-item" id="m15-exec" title="Der Umbau auf ruhende Stop-Orders (05.08.) hatte als Erfolgskriterium: der Abstand Einstieg zum Level muss gegen 0 gehen (vorher Median +0,275xATR). Das ist der groesste je gemessene Hebel des Projekts - und war bis heute unbeobachtet.">—</div>
<div class="m15-item" id="m15-veto" title="Deine eigene Bilanz mit/ohne/gegen Signal, je Lot gerechnet. Gegen das Signal zu handeln kostet gemessen 6,60 EUR je Lot - beidhaelftig robust.">—</div>
</div>
</section>
<section class="card wide" id="card-pos">
<h2 title="Live-Meldungen zum laufenden Trade und zur Marktlage: Squeeze, Überdehnung, S/R-Nähe, News- und Kalender-Konflikte, Hyperliquid-Kontext.">Meldungen</h2>
<div class="squeeze-ind" id="wave-squeeze" title="Volatilitäts-Squeeze: der Kurs komprimiert und bricht aus. Das einzige 2-Stichproben-validierte Einstiegssignal des Projekts."></div>
<div class="bounce-ind" id="wave-bounce" title="Warnung bei Überdehnung — der Kurs ist weit von seiner EMA weg. KEIN Signal: die Richtungstreffer-Quote liegt gemessen bei ~50 %."></div>
<div class="cur-near" id="cur-near" title="Nächstes S/R-Level und Abstand dorthin."></div>
<div class="pos-srhint hidden" id="pos-srhint" title="Nähert sich der Kurs dem Ziel-Level, steht hier die Durchbruch-Wahrscheinlichkeit. Das Modell dahinter ist out-of-sample kalibriert (AUC 0,65)."></div>
<div class="pos-stophint hidden" id="pos-stophint" title="Dasselbe für das Level auf der Verlustseite — reine Anzeige, hier wird nichts automatisch geschlossen."></div>
<div class="news-conflict hidden" id="news-conflict" title="Warnt, wenn der News-Flow deutlich gegen die offene Position oder das aktive Signal steht. Kontext — als Prognose nicht belegt."></div>
<div class="news-conflict hidden" id="cal-note" title="Laufendes oder bevorstehendes High-Impact-Ereignis aus dem Wirtschaftskalender. Sperrt autonome Einstiege, nicht manuelle."></div>
<!-- HL-Kontext (Funding/Open Interest/Oracle-Premium). ⚠ REINE ANZEIGE:
gemessen (`analyze_hl_funding.py`, 5.095 Stundenwerte, 7 Monate)
hält entüberlappt KEIN Bucket die 2-Stichproben-Regel. Kein
Verdict-Gewicht, kein Trigger. -->
<div class="hl-note hidden" id="hl-note"></div>
<!-- Auto-Flip-Close: schließt eine Position, wenn die Empfehlung dreht.
⚠ GEMESSEN in BEIDEN Halbjahren negativ (92/160 R,
backtest_flipclose2.py), läuft auf ausdrücklichen User-Wunsch. Hatte
bis 2026-08-05 KEINE Anzeige — ein Exit, der echtes Geld bewegt und den
niemand sieht. Jetzt sichtbar, damit die Entscheidung informiert ist. -->
<div class="news-conflict hidden" id="flip-note" title="Zustand des Auto-Flip-Close. Derzeit aus — in beiden Halbjahren gemessen negativ."></div>
<div class="reasons" id="wave-reasons" title="Die Begründungen, aus denen sich Richtung und Konfidenz zusammensetzen."></div>
</section>
<!-- Breakout-Autotrader (User-Wunsch 2026-08-19). ⚠ Der autonome Pfad handelt
echtes Geld, seine Abbruchregel (B5) stand bisher NUR im taeglichen
Report. Diese Karte macht ihn sichtbar — Zustand, liegende Orders und
B5-Stand an einer Stelle.
⚠⚠ Sie zeigt BEIDE Zahlen getrennt: die B4-'Mechanik' (Regel neu
gerechnet, sieht gut aus) und die LIVE-Bilanz (verliert). Genau diese
Luecke IST der Befund — eine der beiden allein waere irrefuehrend.
Reine ANZEIGE: schaltet nichts, blockiert nichts. -->
<section class="card wide hidden" id="card-brk">
<h2 title="Autonomer Entry auf den Squeeze-Ausbruch. Einziges 2-Stichproben-validiertes Auto-Setup des Projekts.">🚀 Breakout-Autotrader</h2>
<div class="kv"><span>Zustand</span><b id="brk-state">—</b></div>
<div class="kv"><span>Squeeze jetzt</span><b id="brk-squeeze">—</b></div>
<div class="kv"><span>Liegende Orders</span><b id="brk-pending">—</b></div>
<div class="kv" title="B5 = die VORAB fixierte Abbruchregel: nach 20 Trades ab 01.08. muss Verhaeltnis (OeGewinn/OeVerlust) >= 1,0 UND PF >= 1 sein."><span>B5-Regel</span><b id="brk-b5">—</b></div>
<div id="brk-note" class="hint"></div>
</section>
<section class="card wide" id="card-kontext">
<h2 title="Drei unabhaengige Kontext-Anzeigen. KEINE Empfehlung und keine Richtungsaussage - die Aggregation zu einer Gesamtempfehlung wurde am 08.08. entfernt.">Marktkontext</h2>
<!-- Zeitebenen-Ampel (M1/M5/M15/M30/H1) -->
<div class="tf-turns" id="wave-turns" title="EMA-Stand je Zeitebene: gruen aufwaerts, rot abwaerts. Eine frische Kreuzung blinkt. Reine Anzeige, kein Signal."></div>
<!-- Erwartete Spanne. ⚠ STREUUNG, nicht Richtung - deshalb neutral
gefaerbt. Beschriftet mit der REAL gemessenen Abdeckung (~77 %),
nicht mit dem Nennwert 80 %. Eine von nur zwei out-of-sample
kalibrierten Komponenten des Projekts. -->
<div class="kx-cone" id="kx-cone"></div>
<!-- SELBST-KALIBRIERUNG: die Trefferquote der Empfehlung aus
`rec_outcomes`. Misst das WELLEN-Signal, das bleibt.
⚠ Gegen 50 % zu lesen, nicht gegen 0. -->
<div class="kx-selfcal dim" id="kx-selfcal"></div>
</section>
<!-- Meldungen (ex-Position + Wellen-Meldungen; Welle-Karte entfällt) -->
<!-- Karte „Erwartete Spanne" ENTFERNT 2026-08-05 (User-Vorgabe) — die Kernwerte
stehen jetzt in der Karte „Marktkontext" (`#kx-cone`). Backend
(`core/cone.py`, Snapshot `cone`, MT5-Export `CN;…`) ist unverändert aktiv. -->
<!-- Trailing (Dashboard-Kachel ausgeblendet 2026-07-22, User-Vorgabe; Backend/
Logik unverändert aktiv, nur die Anzeige raus) -->
<section class="card wide hidden" id="card-trail">
<h2 title="Zustand des Trailing-Stops (Phase, aktueller SL, High-Water). Ausgeblendet — das Trailing läuft im Hintergrund weiter.">Trailing</h2>
<div class="kv"><span>Status</span><b id="trail-state">—</b></div>
<div class="kv"><span>Phase</span><b id="trail-phase">—</b></div>
<div class="kv"><span>SL</span><b id="trail-sl">—</b></div>
<div class="kv"><span>TP</span><b id="trail-tp">—</b></div>
<div class="kv"><span>ATR · TF</span><b id="trail-atr">—</b></div>
</section>
<!-- Live-Statistik — ganz unten -->
<section class="card wide" id="card-stats-live">
<h2 title="Kompakte Bilanz über Woche und Gesamt. Verhältnis = Ø-Gewinn geteilt durch Ø-Verlust; unter 1 heißt, die Verlierer sind größer als die Gewinner.">Statistik (live)</h2>
<div class="stx-row" id="stx-row" title="Trades, Trefferquote, Profitfaktor und das Verhältnis von Ø-Gewinn zu Ø-Verlust."><span class="dim">lade …</span></div>
</section>
</main>
<!-- ══ SIG LIVE — Module MIT Verdict-Gewicht (fließen in die Empfehlung ein) ══
Die Zuordnung ist am Code prüfbar: `engine._verdict` ruft `add(name, vote,
gewicht, …)`. Wer hier steht, hat ein Gewicht > 0 und verschiebt die
Bias-Nadel. ⚠ Gewicht heißt NICHT „belegt": der KI-Copilot ist gemessen
nicht robust prädiktiv, hat aber Stimmrecht — genau deshalb ist die
Trennung nach WIRKUNG und nicht nach Beleglage sinnvoll. -->
<section id="view-siglive" class="view hidden">
<div class="view-head">
<h2>SIG Live</h2>
<span class="view-note">Module mit Einfluss auf die Gesamtempfehlung</span>
</div>
<!-- KI-Agent — Verdict-Gewicht 1,0 bei gerichteter Aussage, 0 bei NEUTRAL -->
<section class="card wide" id="card-agent">
<h2 title="Einschätzung des KI-Copiloten (LLM). Stimmt mit Gewicht 1,0 mit, wenn er eine Richtung nennt — bei NEUTRAL zählt er nicht. Als Prognose nicht belegt.">KI-Copilot <span class="ca-tf">Gewicht 1,0</span></h2>
<div class="signal" id="agent-bias" title="Richtungsaussage des Copiloten. NEUTRAL heißt, er zählt in der Empfehlung nicht mit.">◌ —</div>
<div class="agent-head" id="agent-head"></div>
<div class="agent-text" id="agent-reason" title="Die Begründung des Copiloten im Klartext."></div>
<ul class="risks" id="agent-risks"></ul>
</section>
<!-- Chartmuster — Verdict-Gewicht `_PAT_W`=0,25 (User-Vorgabe „5 %" 2026-08-01);
`forming` zählt halb. Deshalb LIVE, nicht Anzeige. -->
<section class="card wide" id="card-patterns">
<h2 title="Mechanisch erkannte Chartmuster auf M30 mit Trigger und Kursziel. Gewicht 0,25. Die Ziel-Trefferquote liegt gemessen nur bei 1338 % — Kontext, keine Vorhersage.">Chartmuster <span class="pt-tf">M30 · Gewicht 0,25</span></h2>
<div class="pt-list" id="pt-list" title="Erkannte Muster mit Skizze, Trigger und Kursziel."><span class="dim">—</span></div>
</section>
</section>
<!-- ══ SIG ANZEIGE — gemessen OHNE Einfluss auf die Empfehlung ══════════════
Kein Verdict-Gewicht, kein Trade-Trigger. Beide Module wurden als Signal
gemessen und verworfen; sie bleiben als Kontext, nicht als Entscheidung. -->
<section id="view-siganzeige" class="view hidden">
<div class="view-head">
<h2>SIG Anzeige</h2>
<span class="view-note">reine Anzeige — gemessen ohne Einfluss</span>
</div>
<!-- Marktstruktur (M30) — Signalvariante gemessen verworfen
(backtest_structure.py + backtest_structure_tf.py: 24/24 Zellen negativ) -->
<section class="card wide" id="card-structure">
<h2 title="Price-Action-Struktur auf M30: Swing-Folge, letzter Bruch (BOS) und Regressionskanal. Reine Anzeige — als Signal über alle Zeitebenen gemessen und verworfen.">Marktstruktur <span class="ms-tf">M30 · nur Anzeige</span></h2>
<div class="ms-top">
<span class="ms-trend" id="ms-trend">—</span>
<div class="ms-swings" id="ms-swings" title="Swing-Folge: HH/HL = Aufwärtsstruktur, LH/LL = Abwärtsstruktur."></div>
</div>
<div class="ms-bos" id="ms-bos" title="Letzter Break of Structure — welches Swing-Level zuletzt gebrochen wurde."></div>
<div class="ms-chan" id="ms-chan" title="Position des Kurses im Regressionskanal: 0 % = unterer Rand, 100 % = oberer.">
<div class="ms-chan-head"><span id="ms-chan-dir">—</span>
<span class="ms-chan-note" id="ms-chan-note"></span></div>
<div class="ms-chan-track"><div class="ms-chan-fill" id="ms-chan-fill"></div>
<div class="ms-chan-dot" id="ms-chan-dot"></div></div>
<div class="ms-chan-scale"><span>Kanal unten</span><span>oben</span></div>
</div>
</section>
<!-- Kerzen-Anatomie (M5) — 3× als Signal gemessen und verworfen; die
Lehrbuch-Deutung ist invertiert, deshalb steht die GEMESSENE Lesart im
Hinweistext und die Kachel ist bewusst neutral gefärbt. -->
<section class="card wide" id="card-candles">
<h2 title="Kerzengröße, Dochte und Volumen der letzten M5-Kerzen. Reine Anzeige und bewusst neutral gefärbt: die Lehrbuch-Lesart ist hier gemessen INVERTIERT — der Kurs läuft nach einer Klimax-Kerze eher GEGEN sie.">Kerzen-Anatomie <span class="ca-tf">M5 · nur Anzeige</span></h2>
<div class="ca-list" id="ca-list" title="Erkannte Kerzenformen der letzten M5-Bars mit Volumen-Vergleich."><span class="dim">—</span></div>
</section>
</section>
<!-- Logs -->
<section id="view-logs" class="view hidden">
<div class="view-head">
<h2>Logs</h2>
<button class="reload" id="logs-reload">↻</button>
</div>
<pre id="logs-body" class="logbox">lade …</pre>
</section>
<!-- Statistik -->
<section id="view-stats" class="view hidden">
<div class="view-head">
<h2>Statistik</h2>
<button class="reload" id="stats-reload">↻</button>
</div>
<div id="sqm-box"><div class="dim" style="padding:8px">lade Monitor …</div></div>
<div id="stats-body"></div>
<h2 class="sub-h">Setups (gesamt)</h2>
<div id="stats-setups"></div>
<!-- ⚠ Aus dem Dashboard hierher verschoben (User-Vorgabe 2026-08-05): das
Dashboard soll die HANDLUNGSrelevanten Module zeigen, die Rückschau
gehört in die Statistik. Die drei Karten werden weiterhin von `render()`
aus dem Snapshot gefüttert — sie hängen NICHT an `loadStats()`. Das
funktioniert, weil ein ausgeblendeter View nur `display:none` ist und
die Elemente im DOM bleiben; sonst würden die IDs beim Rendern fehlen. -->
<!-- Manuelle Trades — deskriptiv (2026-08-04). Zeigt die EIGENE Historie,
rechnet nichts hoch. Der volle Entscheidungskontext (ctx_*) wird erst
seit dem 04.08. mitgeschrieben; die Reifegrad-Zeile macht das sichtbar. -->
<section class="card wide" id="card-manual">
<h2 title="Deine eigenen manuellen Trades im Vergleich zum Bot, aufgeteilt nach Signal-Deckung. Rein deskriptiv.">Manuelle Trades <span class="mt-sub">deine Historie · 30 Tage</span></h2>
<div class="mt-grid" id="mt-grid" title="Deine manuellen Trades über heute, 30 Tage und gesamt, dazu der Bot zum Vergleich."></div>
<div class="mt-split" id="mt-split" title="Aufgeteilt danach, ob der Trade MIT oder OHNE Deckung durch die Empfehlung lief. Gegen das Signal kostet gemessen 6,60 € je Lot."></div>
<div class="mt-note dim" id="mt-note"></div>
</section>
<!-- Letzte Trades -->
<section class="card wide" id="card-trades">
<h2 title="Die zuletzt geschlossenen Trades mit Ergebnis und Schließgrund.">Letzte Trades</h2>
<div class="trades" id="trades" title="Zuletzt geschlossene Trades: Richtung, Ergebnis und Schließgrund."></div>
</section>
<!-- Kurslücken (Gaps) — Fill-Magnete, nur Kontext -->
<section class="card wide" id="card-gaps">
<h2 title="Offene Kurslücken des Jahres. Vakuum = echte Lücke zwischen den Tagesspannen (fließt als S/R in die Konfidenz ein), Eröffnung = Schluss-zu-Eröffnung (reine Anzeige).">Kurslücken (Gaps 2026)</h2>
<div class="gaps-list" id="gaps-list" title="Offene Kurslücken mit Zone, Fill-Level und Abstand zum aktuellen Kurs."><span class="dim">lade …</span></div>
</section>
</section>
<!-- News -->
<section id="view-news" class="view hidden">
<div class="view-head">
<h2>News</h2>
<button class="reload" id="news-reload">↻</button>
</div>
<div id="news-sentiment" class="news-sent"></div>
<div id="news-body"></div>
</section>
<!-- Aktionsleiste (Trade-Steuerung) -->
<nav id="actions">
<!-- Einsatz-Margin manuell vorgeben (User 2026-08-04). Leer/0 = automatisch
(margin_buffer_pct % der freien Margin). ⚠ BEWUSST hier und nicht in der
Trade-Leiste: die ist nur bei OFFENER Position sichtbar, der Einsatz muss
aber VOR dem Öffnen einstellbar sein. Wirkt auf ALLE neuen Positionen. -->
<span class="mg-field" title="Feste Einsatz-Margin in € — leer/0 = automatisch (dann gilt der Prozentsatz rechts). Gilt auch für autonome Trades.">
<label for="margin-input">Einsatz €</label>
<input id="margin-input" type="number" inputmode="decimal" step="10" min="0" placeholder="auto">
</span>
<!-- Prozent-Gegenstück (User 2026-08-05). Steuert `margin_buffer_pct`, das
bisher nur in der ini stand. ⚠ Der feste €-Betrag hat VORRANG — steht er,
wird dieses Feld ausgegraut, damit nicht unklar bleibt, was gilt. -->
<span class="mg-field" id="marginpct-wrap" title="Einsatz als Prozent der freien Margin (199). Greift, solange links kein fester Betrag steht.">
<label for="marginpct-input">Einsatz %</label>
<input id="marginpct-input" type="number" inputmode="decimal" step="5" min="1" max="99" placeholder="95">
</span>
<!-- Raum zum naechsten Level IN Trade-Richtung, direkt auf dem Knopf, den
es betrifft (2026-08-19). ⚠ Gemessen auf 27 EIGENEN Trades: Raum
< 0,6×ATR = 79 % Trefferquote und trotzdem 7,57 bis 11,51 €/Lot
(Klein-Close-Falle — Gewinn am Level gedeckelt, Verlust laeuft bis zum
Stop; ueber 80k Bars als PF 0,42/0,66 belegt). Die Zahl stand bisher
nur in Gate ③b der M15-Karte — nicht dort, wo geklickt wird.
⚠ REINE ANZEIGE: kein Block, keine Sperre, kein Dialog. User-
Uebersteuerungen laufen gemessen 68 % WR. -->
<button class="btn long" data-side="long">▲ LONG<span class="btn-raum" id="raum-long"></span></button>
<button class="btn short" data-side="short">▼ SHORT<span class="btn-raum" id="raum-short"></span></button>
<button class="btn close" data-side="close">✕ CLOSE</button>
<button class="btn tog" id="btn-squeeze" title="Autonomer Entry auf Squeeze-Ausbruch">🚀 BRK</button>
<!-- 🎯 SIG (Auto-Signal-Entry) ENTFERNT 2026-08-12: seit 01.08. aus und auf
BEIDEN Ausfuehrungs-Achsen gemessen negativ (Markt alle 8 Felder,
ruhende Order mit Touch-Fill alle 6 und schlechter als Markt). -->
<button class="btn tog" id="btn-trail">🎯 TRAIL</button>
</nav>
<!-- Modal (Bestätigung / Token-Eingabe) -->
<div id="modal" class="modal hidden">
<div class="box">
<div class="box-title" id="modal-title"></div>
<div class="box-msg" id="modal-msg"></div>
<input id="modal-input" class="hidden" type="password"
placeholder="App-Token" autocomplete="off" autocapitalize="off">
<div class="box-btns">
<button id="modal-cancel" class="mbtn cancel">Abbrechen</button>
<button id="modal-ok" class="mbtn ok">Bestätigen</button>
</div>
</div>
</div>
<div id="toast" class="toast hidden"></div>
<script src="app.js?v=176"></script>
</body>
</html>