Entry-Checkliste im Order-Dialog + Liquiditaets-Pfeile rechtsbuendig (v=120, MQL5 v1.14)

1) Entry-Checkliste (User: "blende einen Hinweis ein wenn die Checkliste gegen
   meine Eroeffnung ist"): engine._entry_checklist prueft 7 Punkte mit Live-Daten
   -- Signal-Deckung, HTF-Trend M30+H1, Entry-Raum, News-Konflikt, Kosten,
   Verlust-Serie heute (Mental-Check), Nacht-Stunde. Snapshot checklist{long,short},
   Anzeige im Bestaetigungs-Dialog, blockt nicht. Kommt jetzt auch bei vorhandenem
   Signal (alter Dialog nur bei WARTEN/Gegen-Signal).
   Real beim Einbau: beide Richtungen "stop" (11 Verluste heute = 80% des Kontos).

2) MQL5 v1.14 (User: "blende die liquiditaetslinien aus, nur die Pfeile sollen
   rechtsbuendig angezeigt werden"): LQ-Linien + Text-Labels raus, nur noch der
   Pfeil auf dem Wand-Preis, rechtsbuendig am aktuellen Rand (in RepositionLabels
   nachgefuehrt, bleibt beim Scrollen rechts). Details im Tooltip. Pinke
   Liquiditaets-Trendlinie unveraendert. Kompiliert.

3) backtest_liquidity_sweep.py (SMC Equal Highs/Lows + Swing Liquidity, Multi-TF-
   und Session-Achse): faellt durch -- Details folgen in der Doku.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-07-30 18:15:03 +02:00
co-authored by Claude Opus 4.8
parent b77b3d60f5
commit 919fe61dc3
5 changed files with 531 additions and 43 deletions
+142 -1
View File
@@ -2088,6 +2088,133 @@ class TradingEngine:
self._align_ts = now
return getattr(self, "_align_cache", None)
def _loss_streak_today(self) -> tuple[int, int]:
"""(Verlust-Trades heute, Trades heute) — für den Mental-Check der Entry-
Checkliste („nach 2 Losses Session beenden"). DB-Query ~120 s gecacht."""
now = time.time()
if now - getattr(self, "_streak_ts", 0.0) > 120:
try:
import datetime as _dt
t0 = _dt.datetime.now(_BERLIN).replace(
hour=0, minute=0, second=0, microsecond=0).timestamp()
with self.history._connect() as conn:
rows = conn.execute(
"SELECT pnl FROM trades WHERE exit_time>=? AND pnl IS NOT NULL",
(t0,)).fetchall()
self._streak_cache = (sum(1 for r in rows if (r[0] or 0) < 0), len(rows))
except Exception as e:
log.debug(f"loss_streak: {e}")
self._streak_cache = (0, 0)
self._streak_ts = now
return getattr(self, "_streak_cache", (0, 0))
def _entry_checklist(self, side: str, snap_parts: dict) -> dict:
"""Prüft die Trade-Setup-Checkliste (User-Vorgabe 2026-07-30) für eine
geplante Eröffnung und liefert die VERLETZTEN Punkte zurück.
Warum server-seitig: die Daten liegen alle hier (TF-Ampel, Struktur, Level,
News, Tages-P&L, Trade-Historie) — das Frontend soll nur anzeigen.
Jeder Punkt: `state` = "fail" (klar dagegen) | "warn" (Vorsicht) | "ok".
Reine ENTSCHEIDUNGSHILFE — sie blockt nichts (wie der bisherige Dialog),
macht aber sichtbar, was gegen den Trade spricht. Die Gewichtung ist NICHT
aus einem Backtest, sondern aus (a) den im Projekt GEMESSENEN Leckagen
(Handeln ohne Signal, Nacht/teure Stunden, Verlust-Serien) und (b) der
vom User vorgegebenen Checkliste."""
want = "LONG" if side == "long" else "SHORT"
d = 1 if want == "LONG" else -1
items: list[dict] = []
add = lambda k, st, lab, det: items.append(
{"key": k, "state": st, "label": lab, "detail": det})
wave = snap_parts.get("wave") or {}
market = snap_parts.get("market") or {}
# ── 1. Signal-Deckung (die gemessene Kern-Leckage) ───────────────────
sig = (wave.get("signal") or "WARTEN") if isinstance(wave.get("signal"), str) \
else ((wave.get("signal") or {}).get("signal") or "WARTEN")
if sig == want:
add("signal", "ok", "Empfehlung deckt den Trade", f"Signal {sig}")
elif sig in ("WARTEN", "", None):
add("signal", "fail", "Keine Empfehlung (WARTEN)",
"gemessen: Trades ohne Signal sind die größte Verlustquelle")
else:
add("signal", "fail", f"Empfehlung steht GEGEN dich ({sig})",
"gemessen: 40 % Trefferquote gegen das Signal")
# ── 2. HTF-Trend klar? (Checklisten-Punkt 1) ─────────────────────────
turns = wave.get("tf_turns") or {}
htf = [turns.get(t, {}).get("dir") if isinstance(turns.get(t), dict)
else turns.get(t) for t in ("M30", "H1")]
htf = [x for x in htf if x in (1, -1, 0)]
if htf and all(x == d for x in htf):
add("htf", "ok", "HTF-Trend (M30+H1) trägt die Richtung", "")
elif htf and any(x == -d for x in htf):
add("htf", "fail", "HTF-Trend steht dagegen (M30/H1)",
"deine Regel: nur MIT dem HTF-Trend, niemals dagegen")
else:
add("htf", "warn", "HTF-Trend unklar/seitwärts", "kein klarer M30/H1-Trend")
# ── 3. Entry-Raum bis zum Gegenlevel (gemessen monoton) ──────────────
try:
dl = self._draw_levels()
atr = float((wave.get("pb_feats") or {}).get("atr") or 0.0) or 0.2
bid = market.get("bid")
tgt = (dl.get("res") or [None])[0] if d > 0 else (dl.get("sup") or [None])[0]
if bid and tgt:
room = abs(tgt - bid) / atr
if room < 0.6:
add("room", "fail", f"Kein Raum: Level nur {room:.1f}×ATR entfernt",
"gemessen: <0,6×ATR Raum = negativer Erwartungswert")
elif room < 1.0:
add("room", "warn", f"Wenig Raum ({room:.1f}×ATR bis zum Level)", "")
else:
add("room", "ok", f"Raum bis zum Level: {room:.1f}×ATR", "")
except Exception:
pass
# ── 4. News-Konflikt (Checklisten-Punkt 3) ───────────────────────────
ns = snap_parts.get("news_sentiment")
if isinstance(ns, (int, float)) and abs(ns) >= 0.5:
if (ns > 0) != (d > 0):
add("news", "fail",
f"News-Flow steht dagegen ({ns:+.2f})",
"stark bullische/bärische Schlagzeilen gegen deine Richtung")
else:
add("news", "ok", f"News-Flow stützt ({ns:+.2f})", "")
# ── 5. Kosten (Spread/ATR — gemessen Ø 0,265) ────────────────────────
cr = snap_parts.get("cost_ratio")
if isinstance(cr, (int, float)) and cr > 0:
if cr >= 0.32:
add("cost", "fail", f"Kosten hoch ({cr:.2f}×ATR)",
"gemessen: ab ~0,32×ATR frisst der Spread den Edge")
elif cr >= 0.20:
add("cost", "warn", f"Kosten erhöht ({cr:.2f}×ATR)", "")
else:
add("cost", "ok", f"Kosten günstig ({cr:.2f}×ATR)", "")
# ── 6. Mental-Check: Verlust-Serie heute (Checklisten-Punkt 5) ───────
losses, total = self._loss_streak_today()
day_total = float((snap_parts.get("day_pl") or {}).get("total") or 0.0)
if losses >= 2:
bal = float(self.data.balance or 0.0)
pct = (abs(day_total) / bal * 100.0) if (bal and day_total < 0) else 0.0
add("streak", "fail",
f"Heute schon {losses} Verluste ({total} Trades, {day_total:+.0f}"
+ (f" = {pct:.0f}% des Kontos)" if pct else ")"),
"deine Regel: nach 2 Losses Session beenden")
elif day_total < 0:
add("streak", "warn", f"Tag im Minus ({day_total:+.0f} €)", "")
else:
add("streak", "ok", f"Tagesbilanz {day_total:+.0f}", "")
# ── 7. Nacht-/Kostenfalle (gemessen: 07 Uhr Berlin) ─────────────────
try:
hb = datetime.now(_BERLIN).hour
if hb in _SQUEEZE_NIGHT:
add("hour", "fail", f"Nacht-Stunde ({hb}:00 Berlin)",
"gemessen: 07 Uhr = Kostenfalle (Spread ÷ dünner ATR)")
except Exception:
pass
fails = sum(1 for x in items if x["state"] == "fail")
warns = sum(1 for x in items if x["state"] == "warn")
return {"side": want, "items": items, "fails": fails, "warns": warns,
"verdict": ("stop" if fails >= 2 else "warn" if (fails or warns) else "ok")}
def snapshot_cached(self) -> dict:
"""Teilt EINEN frisch berechneten Snapshot über alle WS-Clients/HTTP-
Aufrufe (TTL ~0,8 s). Spart SQLite/Verdict-Last bei mehreren Geräten —
@@ -2309,7 +2436,7 @@ class TradingEngine:
}
sr_close_hint = self._sr_close_hint(market, position, wave_snap)
stop_approach = self._stop_approach_hint(market, position, wave_snap)
return {
snap = {
"ts": time.time(),
"connected": self._connected,
"running": self._running,
@@ -2380,6 +2507,20 @@ class TradingEngine:
"adverse15_count": self._adverse15_count,
"last_trades": last_trades,
}
# ── Entry-Checkliste je Richtung (User-Vorgabe 2026-07-30: „blende einen
# Hinweis ein, wenn die Checkliste gegen meine Eröffnung ist"). Wird im
# Order-Dialog angezeigt; blockt NICHT. Nutzt nur bereits berechnete Teile
# → kein zusätzlicher MT5-/DB-Zugriff im Snapshot-Pfad.
try:
_parts = {"wave": wave_snap, "market": market,
"news_sentiment": snap.get("news_sentiment"),
"cost_ratio": snap.get("cost_ratio"), "day_pl": day_pl}
snap["checklist"] = {"long": self._entry_checklist("long", _parts),
"short": self._entry_checklist("short", _parts)}
except Exception as e:
log.debug(f"entry_checklist: {e}")
snap["checklist"] = None
return snap
def _sr_close_hint(self, market, position, wave_snap) -> dict | None:
"""S/R-Close-Empfehlung mit kalibriertem P(Durchbruch)-Modell (gemessen