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.agent import TradingAgent
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.mt5_utils import mt5_lock
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))))
# --- 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:
"""Headless-Pendant zu OilWidget: hält die core-Objekte, fährt die
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.
try:
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):
self._cb_limit_pct = 0.0
self._circuit_tripped_date = None # Berlin-Datum, an dem der Breaker griff
@@ -1024,7 +1061,10 @@ class TradingEngine:
if err:
log.error(f"{ok_word} fehlgeschlagen: {err}")
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})",
tg["bot_token"], tg["chat_id"])
except Exception as e: log.warning(f"Auto-Close-Telegram (Fehler): {e}")
@@ -1453,6 +1493,33 @@ class TradingEngine:
self._save_runtime_state()
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):
"""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
@@ -2094,6 +2161,10 @@ class TradingEngine:
if "manual_margin" in st:
from core.config import set_manual_margin as _set
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:
from core.config import set_margin_buffer as _setp, get_margin_buffer
_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,
"manual_margin": getattr(self, "_manual_margin", 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
# die muehsam erlernte Kalibrierung nach jedem Neustart
# 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}
@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")
async def toggle_autosqueeze(x_auth_token: str | None = Header(default=None)):
"""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;
shortBtn.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
// 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
@@ -1306,32 +1317,6 @@ function render(d) {
if (smi && document.activeElement !== smi) {
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)
const src = d.sr_close_count;
if (src != null) {
@@ -1744,7 +1729,7 @@ $("mute-btn").onclick = () => {
_updateMuteBtn();
// 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)
$("btn-trail").onclick = () => toggle("api/trail", "Trailing");
$("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(); }
});
// Feste Einsatz-Margin (User-Wunsch 2026-08-04). Leer/0 = automatisch.
// ⚠ Wirkt auf ALLE neuen Positionen, auch die autonomen — deshalb sagt der Toast
// ausdrücklich, was gilt, statt nur „gespeichert".
async function setManualMargin() {
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(); }
});
})();
// Einsatz-Felder ENTFERNT (User 2026-08-19). Das BACKEND bleibt vollstaendig:
// beide Endpunkte sind weiter erreichbar, die Werte leben in
// `runtime_state.json` UND der ini (aktuell 40 %). Ohne UI aendert man den
// Prozentsatz also dort + Neustart -- oder per POST.
// Notfall-Stop: manueller Verlust-Wert, bei dem der Server sofort schließt.
// 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
und muss offline funktionieren. -->
<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>
<body>
<!-- Modul-Menü (ganz oben) -->
@@ -339,33 +339,15 @@
<!-- 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. -->
<!-- Einsatz-Felder ENTFERNT (User 2026-08-19). Der Einsatz steht auf
40 % (ini + `runtime_state.json`); ohne UI aendert man ihn dort
+ Neustart. -->
<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>
<!-- 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
BEIDEN Ausfuehrungs-Achsen gemessen negativ (Markt alle 8 Felder,
ruhende Order mit Touch-Fill alle 6 und schlechter als Markt). -->
@@ -387,6 +369,6 @@
</div>
<div id="toast" class="toast hidden"></div>
<script src="app.js?v=176"></script>
<script src="app.js?v=177"></script>
</body>
</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>