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:
co-authored by
Claude Opus 4.8
parent
b77b3d60f5
commit
919fe61dc3
+142
-1
@@ -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: 0–7 Uhr Berlin) ─────────────────
|
||||
try:
|
||||
hb = datetime.now(_BERLIN).hour
|
||||
if hb in _SQUEEZE_NIGHT:
|
||||
add("hour", "fail", f"Nacht-Stunde ({hb}:00 Berlin)",
|
||||
"gemessen: 0–7 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
|
||||
|
||||
Reference in New Issue
Block a user