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:
co-authored by
Claude Opus 5
parent
32c064a1a6
commit
e48ad80c0a
+77
-2
@@ -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
@@ -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."""
|
||||||
|
|||||||
@@ -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 1–99 % 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
@@ -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 (1–99). "
|
|
||||||
+ "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 1–99 % 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
@@ -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 (1–99). 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>
|
||||||
|
|||||||
@@ -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 13–38 % — 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 (1–99). 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>
|
||||||
Reference in New Issue
Block a user