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
+316
View File
@@ -0,0 +1,316 @@
#!/usr/bin/env python3
"""backtest_liquidity_sweep.py — Liquiditäts-Sweeps (SMC/ICT) messen (B2).
User-Vorgabe 2026-07-30 (mit Chart-Beispielen): die drei „wichtigsten Liquiditäts-
muster" für WTI in die Gesamtempfehlung aufnehmen —
A) **Equal Highs → Sell-Side-Sweep**: zwei ~gleiche Hochs bilden eine Stop-Zone;
der Kurs schießt KURZ darüber (Sweep), schließt zurück darunter → SHORT.
B) **Equal Lows → Buy-Side-Sweep**: Spiegelbild → LONG.
C) **Swing Liquidity**: markanter Swing-Punkt wird gesweept/retestet, dann Trade
in Richtung des höheren Timeframe-Trends (hier M30-EMA12/50 als HTF-Proxy).
⚠ WARUM das trotz 15 verworfener Signal-Ideen einen eigenen Test verdient: Der
**Sweep ist ein FEHLAUSBRUCH** (Level kurz durchstochen, Schluss zurück im Bereich) —
eine andere Mechanik als die bereits verworfenen Muster (`backtest_doubletop.py` misst
den Nackenlinien-BRUCH, nicht den Fehlausbruch) und als die Struktur-/Volume-Profile-
Klasse. Verwandt ist er mit dem validierten P(break)-Modell: ein Sweep ist genau der
Fall „Level wird berührt/überschritten, hält aber" — dort ist die Vorhersage bereits
belegt (AUC 0,72), allerdings nur als EXIT, nicht als Entry.
Methodik (wie alle Tests hier): Signal-TF M5, sequentielle 1-Positions-Sim, Live-Exit
(SL 2,0×ATR · Trail 1,5 ab 0,3 · Breakeven 1,3 · Time-Stop 120 min), Kosten = echter
Bar-Spread/ATR, 2 Halbjahre. Zusätzlich wird der SMC-eigene Exit geprüft
(Stop über/unter dem Sweep-Docht, Ziel = gegenüberliegende Liquidität), weil das
Setup laut Lehre damit steht und fällt.
Maßstab: der einzige validierte Auto-Entry (Squeeze) liefert ØR +0,14…+0,23 · PF>1
in BEIDEN Hälften. Alles darunter ist kein Kandidat für die Gesamtempfehlung.
Die vom User nachgereichten WTI-Praxisregeln sind als ACHSEN eingebaut, statt sie
zu glauben:
· **Multi-TF** („Liquidität auf 4H/1H identifizieren, Entry auf 15M/5M") → die
Level-TF ist variabel (M5 / M30 / H1). Passt zum eigenen Befund von 2026-07-30
(`backtest_level_tf.py`): M30-Level schlagen M5-Level deutlich.
· **Session-Timing** („Sweeps um 8:3010:30 NY = 14:3016:30 Berlin, oder 24 Uhr
asiatische Liquiditätsjagd") → Session-Filter als eigene Variante.
· **„Nicht jeder Sweep dreht — warte auf den Close zurück"** → ist die harte
Bedingung dieses Tests (Close muss im selben Bar zurück in der Zone sein).
· **DXY-Korrelation** → NICHT eingebaut: die Inter-Market-Klasse (Brent/DXY) wurde
2026-07-19 gemessen und verworfen (`backtest_intermarket.py`, kippt zwischen den
Hälften). Ohne neuen Beleg keine zweite Runde.
· **„Sweep-Wick 0,501,00 $, kleiner sizen"** → im Test implizit über den
SMC-Exit (Stop am Docht) abgebildet; Sizing ist hier neutral (R-Rechnung).
Aufruf: python backtest_liquidity_sweep.py [n_bars]
"""
from __future__ import annotations
import sys
import MetaTrader5 as mt5
# ── Muster-Parameter (bewusst wenige, sonst Overfitting) ─────────────────────
_PIV_K = 3 # Swing-Pivot-Fenster
_EQ_TOL = 0.15 # „gleich" = beide Hochs/Tiefs innerhalb x×ATR
_EQ_MINGAP = 3 # min. Bars zwischen den beiden Pivots (sonst dieselbe Zacke)
_EQ_MAXGAP = 60 # max. Bars — danach ist die Zone kalt
_SWEEP_MIN = 0.05 # Sweep muss das Level um ≥ x×ATR überschießen (echter Stop-Grab)
_SWEEP_MAX = 1.00 # aber nicht mehr als x×ATR (sonst echter Ausbruch, kein Sweep)
_BACK_BARS = 3 # innerhalb so vieler Bars muss der Close zurück in die Zone
_LOOKBACK = 300 # Pivot-Suchfenster
_COOL = 12 # Cooldown zwischen Trades (Bars)
# Session-Fenster in BERLIN-Stunden (Broker ist UTC+3 → Berlin = Broker 1 h).
# NY 8:3010:30 ET ≈ 14:3016:30 Berlin (EIA/Market-Open), Asia-Jagd 24 Uhr.
_SESS = {"NY": (14, 15, 16), "Asia": (2, 3, 4), "NY+Asia": (2, 3, 4, 14, 15, 16)}
_LEVEL_TFS = (("M5", 1), ("M30", 6), ("H1", 12)) # Level-Quelle (Multi-TF-Regel)
# ── Exit (Live-Modell, identisch zu den anderen Backtests) ───────────────────
_SL_ATR, _TRAIL, _TRAIL_ON, _BE_ON, _TIMESTOP, _MAXH = 2.0, 1.5, 0.3, 1.3, 24, 288
_ATRMIN = 0.06
def _atr_series(H, L, C, p=14):
tr = [0.0]
for i in range(1, len(C)):
tr.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1])))
out = [None] * len(C); run = 0.0
for i in range(1, len(C)):
run += tr[i]
if i > p: run -= tr[i - p]
out[i] = run / min(i, p)
return out
def _ema_series(v, p):
k = 2.0 / (p + 1); o = [v[0]]
for x in v[1:]:
o.append(x * k + o[-1] * (1 - k))
return o
def _sim_live(entry, d, atr, H, L, C, j0):
"""Live-Exit: SL 2×ATR, Trailing 1,5 ab 0,3, Breakeven 1,3, Time-Stop."""
eff = entry - d * _SL_ATR * atr; hw = entry; started = False
end = min(j0 + _MAXH, len(C) - 1); px = C[end]
for j in range(j0, end + 1):
if (L[j] <= eff) if d > 0 else (H[j] >= eff): px = eff; break
hw = max(hw, H[j]) if d > 0 else min(hw, L[j])
prof = (C[j] - entry) * d
if prof >= _TRAIL_ON * atr:
started = True
cand = hw - d * _TRAIL * atr
if prof >= _BE_ON * atr:
cand = max(cand, entry) if d > 0 else min(cand, entry)
eff = max(eff, cand) if d > 0 else min(eff, cand)
if not started and (j - j0) >= _TIMESTOP: px = C[j]; break
return (px - entry) * d / atr
def _sim_smc(entry, d, atr, stop, target, H, L, C, j0):
"""SMC-Exit laut Lehre: Stop über/unter dem Sweep-Docht, Ziel = gegenüber-
liegende Liquidität. Ohne Ziel → Live-Exit-Fallback."""
if target is None:
return _sim_live(entry, d, atr, H, L, C, j0)
end = min(j0 + _MAXH, len(C) - 1); px = C[end]
for j in range(j0, end + 1):
hit_stop = (L[j] <= stop) if d > 0 else (H[j] >= stop)
hit_tgt = (H[j] >= target) if d > 0 else (L[j] <= target)
# konservativ: bei beidem im selben Bar zählt der Stop
if hit_stop: px = stop; break
if hit_tgt: px = target; break
return (px - entry) * d / atr
def _rep(name, Rs):
if not Rs or len(Rs) < 10:
print(f" {name:<40} n={len(Rs):>4} (zu wenige)"); return None
n = len(Rs); w = sum(1 for x in Rs if x > 0); s = sum(Rs)
up = sum(x for x in Rs if x > 0); dn = -sum(x for x in Rs if x < 0)
pf = up / dn if dn > 0 else 9.99
print(f" {name:<40} n={n:>4} WR={100*w/n:>3.0f}% ØR={s/n:+.3f} "
f"PF={pf:.2f} ΣR={s:+.0f} worst={min(Rs):+.1f}")
return s / n
def find_pivots(H, L, step=1):
"""Pivots auf der aggregierten Level-TF; Index = M5-Index, ab dem der Pivot
BEKANNT ist (Bestätigung braucht k Bars danach) → kein Look-ahead."""
if step == 1:
ph = [(i, H[i]) for i in range(_PIV_K, len(H) - _PIV_K)
if H[i] == max(H[i - _PIV_K:i + _PIV_K + 1])]
pl = [(i, L[i]) for i in range(_PIV_K, len(L) - _PIV_K)
if L[i] == min(L[i - _PIV_K:i + _PIV_K + 1])]
return ph, pl
hh, ll = [], []
for s0 in range(0, len(H) - step + 1, step):
hh.append(max(H[s0:s0 + step])); ll.append(min(L[s0:s0 + step]))
ph, pl = [], []
for j in range(_PIV_K, len(hh) - _PIV_K):
avail = (j + _PIV_K + 1) * step
if hh[j] == max(hh[j - _PIV_K:j + _PIV_K + 1]): ph.append((avail, hh[j]))
if ll[j] == min(ll[j - _PIV_K:j + _PIV_K + 1]): pl.append((avail, ll[j]))
return ph, pl
def scan(H, L, C, SP, A, EF30, ES30, lo_i, hi_i, mode, smc_exit,
step=1, hours=None, BH=None):
"""mode: 'eqh' (Equal Highs→Short), 'eql' (Equal Lows→Long), 'swing' (HTF-Trend).
`step` = Level-TF (1=M5, 6=M30, 12=H1) · `hours` = erlaubte Berlin-Stunden
(None = alle). Ein Pivot ist erst ab (idx+k+1) bekannt → kein Look-ahead."""
ph, pl = find_pivots(H, L, step)
Rs = []
def cost(i, atr): return (SP[i] if SP[i] > 0 else 0.0225) / atr
i = max(lo_i, _LOOKBACK)
hi_i = min(hi_i, len(C) - _MAXH - 2)
while i < hi_i:
atr = A[i]
if not atr or atr < _ATRMIN: i += 1; continue
if hours is not None and BH is not None and BH[i] not in hours:
i += 1; continue
tol = _EQ_TOL * atr
# ── Liquiditätszone bestimmen ────────────────────────────────────────
level = None; d = 0; zone_lo = None
if mode in ("eqh", "swing"):
# bekannte Hoch-Pivots im Fenster (bestätigt vor i)
hs = [(ix, p) for ix, p in ph if i - _LOOKBACK <= ix <= i - _PIV_K - 1]
if mode == "eqh" and len(hs) >= 2:
# zwei ~gleiche Hochs mit passendem Abstand
for a in range(len(hs) - 1, 0, -1):
for b in range(a - 1, -1, -1):
gap = hs[a][0] - hs[b][0]
if gap < _EQ_MINGAP: continue
if gap > _EQ_MAXGAP: break
if abs(hs[a][1] - hs[b][1]) <= tol:
level = max(hs[a][1], hs[b][1]); d = -1; break
if level is not None: break
elif mode == "swing" and hs:
level = hs[-1][1]; d = -1
if mode in ("eql", "swing") and level is None:
ls = [(ix, p) for ix, p in pl if i - _LOOKBACK <= ix <= i - _PIV_K - 1]
if mode == "eql" and len(ls) >= 2:
for a in range(len(ls) - 1, 0, -1):
for b in range(a - 1, -1, -1):
gap = ls[a][0] - ls[b][0]
if gap < _EQ_MINGAP: continue
if gap > _EQ_MAXGAP: break
if abs(ls[a][1] - ls[b][1]) <= tol:
level = min(ls[a][1], ls[b][1]); d = 1; break
if level is not None: break
elif mode == "swing" and ls:
level = ls[-1][1]; d = 1
if level is None or d == 0: i += 1; continue
# ── Sweep prüfen: Bar i durchsticht das Level, Close kommt zurück ────
if d < 0: # Equal Highs: High über Level, Close zurück darunter
over = H[i] - level
swept = (_SWEEP_MIN * atr <= over <= _SWEEP_MAX * atr) and C[i] < level
wick = H[i]
else: # Equal Lows: Low unter Level, Close zurück darüber
over = level - L[i]
swept = (_SWEEP_MIN * atr <= over <= _SWEEP_MAX * atr) and C[i] > level
wick = L[i]
if not swept:
i += 1; continue
# „Close zurück in die Zone" darf auch 1_BACK_BARS Bars dauern; hier ist er
# bereits im Sweep-Bar erfüllt (strengste, sauberste Variante).
# Swing-Modus: nur MIT dem HTF-Trend (M30-EMA12/50) handeln
if mode == "swing":
htf = 1 if EF30[i] > ES30[i] else -1 if EF30[i] < ES30[i] else 0
if htf != d: i += 1; continue
entry = C[i]
stop = wick + d * -1 * (0.05 * atr) # knapp jenseits des Sweep-Dochts
stop = (wick - 0.05 * atr) if d > 0 else (wick + 0.05 * atr)
# Ziel = gegenüberliegende Liquidität (nächster Pivot in Trade-Richtung)
if d < 0:
cands = [p for ix, p in pl if i - _LOOKBACK <= ix <= i - _PIV_K - 1
and p < entry - 0.3 * atr]
target = max(cands) if cands else None
else:
cands = [p for ix, p in ph if i - _LOOKBACK <= ix <= i - _PIV_K - 1
and p > entry + 0.3 * atr]
target = min(cands) if cands else None
R = (_sim_smc(entry, d, atr, stop, target, H, L, C, i + 1) if smc_exit
else _sim_live(entry, d, atr, H, L, C, i + 1)) - cost(i, atr)
Rs.append(R)
i += _COOL
return Rs
def main():
n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000
mt5.initialize(); sym = None
for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD"):
if mt5.symbol_info(c): sym = c; break
bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n)
si = mt5.symbol_info(sym); point = si.point
mt5.shutdown()
H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars]
C = [float(b["close"]) for b in bars]; SP = [float(b["spread"]) * point for b in bars]
A = _atr_series(H, L, C)
# HTF-Proxy für den Swing-Modus: M30 aus M5 aggregiert (6 Bars), auf M5 gemappt
c30 = [C[s + 5] for s in range(0, len(C) - 5, 6)]
e12 = _ema_series(c30, 12); e50 = _ema_series(c30, 50)
EF30 = [0.0] * len(C); ES30 = [0.0] * len(C)
for i in range(len(C)):
j = i // 6 - 1
if 0 <= j < len(e12): EF30[i] = e12[j]; ES30[i] = e50[j]
# Berlin-Stunde je Bar (Broker UTC+3 → Berlin = Broker 1 h, Sommerzeit)
from datetime import datetime, timezone
BH = [(datetime.fromtimestamp(int(b["time"]), timezone.utc).hour - 1) % 24
for b in bars]
N = len(C); mid = N // 2
print("=" * 96)
print(f" LIQUIDITÄTS-SWEEPS (SMC) — {sym} M5 ({N} Bars, 2 Halbjahre)")
print(f" Sweep = Level um {_SWEEP_MIN}{_SWEEP_MAX}×ATR durchstochen, Close zurück in der Zone")
print(f" Maßstab: validierter Squeeze ØR +0,14…+0,23 · PF>1 in BEIDEN Hälften")
print("=" * 96)
verdict = {}
MODES = (("eqh", "A) Equal Highs -> SHORT"),
("eql", "B) Equal Lows -> LONG"),
("swing", "C) Swing-Liquidity (HTF)"))
# ── Achse 1: LEVEL-TF (Multi-TF-Regel „Liquidität auf höherem TF") × Exit ──
for tf_lbl, step in _LEVEL_TFS:
print("\n" + "=" * 96)
print(f"### LEVEL-TF {tf_lbl} — Liquidität auf {tf_lbl} identifiziert, Entry auf M5")
for exit_lbl, smc in (("Live-Exit", False), ("SMC-Exit", True)):
print(f"\n --- EXIT: {exit_lbl} ---")
for mode, mlabel in MODES:
for hl, lo, hi in (("H1", _LOOKBACK, mid), ("H2", mid, N)):
Rs = scan(H, L, C, SP, A, EF30, ES30, lo, hi, mode, smc, step)
oR = _rep(f"{mlabel} [{hl}]", Rs)
verdict.setdefault((f"{tf_lbl}/{exit_lbl}", mlabel), []).append(oR)
# ── Achse 2: SESSION-Filter (Level-TF M30, Live-Exit) ────────────────────
print("\n" + "=" * 96)
print("### SESSION-FILTER (Level-TF M30, Live-Exit) — „Sweeps passieren um 1416 / 24 Uhr\"")
for sess, hours in _SESS.items():
print(f"\n --- Session: {sess} (Berlin-Stunden {hours}) ---")
for mode, mlabel in MODES[:2]:
for hl, lo, hi in (("H1", _LOOKBACK, mid), ("H2", mid, N)):
Rs = scan(H, L, C, SP, A, EF30, ES30, lo, hi, mode, False, 6, hours, BH)
oR = _rep(f"{mlabel} [{hl}]", Rs)
verdict.setdefault((f"M30/Sess-{sess}", mlabel), []).append(oR)
print("\n" + "=" * 96)
print(" URTEIL (Kandidat für die Gesamtempfehlung nur, wenn ØR in BEIDEN Hälften")
print(" > 0 UND ≥ +0,14 = Squeeze-Band):")
any_ok = False
for (ex, m), vals in verdict.items():
if len(vals) != 2 or any(v is None for v in vals):
print(f" {m:<38} {ex[:22]:<24} unvollständig"); continue
ok = all(v > 0 for v in vals); band = all(v >= 0.14 for v in vals)
mark = "✅ TRÄGT" if (ok and band) else ("⚠ positiv, unter Band" if ok else "❌ fällt durch")
if ok and band: any_ok = True
print(f" {m:<38} {ex[:22]:<24} H1 {vals[0]:+.3f} · H2 {vals[1]:+.3f} {mark}")
print("\n " + ("→ Kandidat gefunden: Parameter-Robustheit prüfen, dann als Verdict-Modul."
if any_ok else
"→ KEINE Variante trägt beidhälftig im Squeeze-Band → nicht in die "
"Gesamtempfehlung (höchstens als Anzeige)."))
if __name__ == "__main__":
main()
+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
+40 -38
View File
@@ -12,7 +12,13 @@
//| 4) Der Bot muss laufen (schreibt die CSV alle ~5 s). |
//+------------------------------------------------------------------+
#property copyright "Oil Trading Bot"
#property version "1.13"
#property version "1.14"
// v1.14 (2026-07-30): Liquiditäts-LINIEN + Text-Labels ENTFERNT (User: „blende die
// liquiditätslinien aus, nur die Pfeile sollen rechtsbündig angezeigt werden").
// Es bleibt NUR der Pfeil auf dem Wand-Preis, RECHTSBÜNDIG am aktuellen Rand
// (wird in RepositionLabels() nachgeführt, bleibt beim Scrollen rechts).
// Details (Seite/Größe/Dominanz/Preis) jetzt im TOOLTIP statt im Chart.
// Die pinke Liquiditäts-Trendlinie bleibt unverändert.
// v1.13 (2026-07-30): Liquiditäts-Wände bekommen einen PFEIL in ihrer Wirkrichtung
// (Lesart „die Wand hält" — Nachfrage stützt hoch, Angebot deckelt runter;
// die GRÖSSERE Wand wird dicker gezeichnet + "<<"). NEU: pinke
@@ -95,6 +101,17 @@ void RepositionLabels()
ObjectMove(0, nm, 0, ct, pr);
}
}
// Liquiditäts-PFEILE rechtsbündig halten: die CSV ändert sich nicht bei jedem
// Tick, ohne Nachführung würden die Pfeile mit jeder neuen Kerze nach links
// wandern. Preis bleibt, nur die Zeit wird an den rechten Rand gezogen.
datetime rt = iTime(_Symbol, PERIOD_CURRENT, 0) + (datetime)(PeriodSeconds() * 2);
int na = ObjectsTotal(0, 0, OBJ_ARROW);
for(int i = 0; i < na; i++)
{
string nm = ObjectName(0, i, 0, OBJ_ARROW);
if(StringFind(nm, PFX + "ALQ_") == 0)
ObjectMove(0, nm, 0, rt, ObjectGetDouble(0, nm, OBJPROP_PRICE));
}
ChartRedraw();
}
//+------------------------------------------------------------------+
@@ -277,54 +294,39 @@ void Redraw()
if(lpx <= 0) continue;
bool isBid = (side == "B");
color lcol = isBid ? InpWallBidColor : InpWallAskColor;
string nm = PFX + "LQ_" + side;
if(ObjectCreate(0, nm, OBJ_HLINE, 0, 0, lpx))
{
ObjectSetInteger(0, nm, OBJPROP_COLOR, lcol);
ObjectSetInteger(0, nm, OBJPROP_STYLE, STYLE_DASH);
ObjectSetInteger(0, nm, OBJPROP_WIDTH, 2);
ObjectSetInteger(0, nm, OBJPROP_BACK, true);
ObjectSetInteger(0, nm, OBJPROP_SELECTABLE, false);
}
else
ObjectMove(0, nm, 0, 0, lpx);
// Pfeil = Wirkrichtung der Wand (Feld 4: U/D). Lesart „die Wand HÄLT":
// Nachfrage-Wand stützt von unten (hoch), Angebots-Wand deckelt (runter).
// Feld 5 = 1 bei der GRÖSSEREN der beiden Wände → dickere Linie + "<<".
// ⚠ 2026-07-30 (User): KEINE waagerechte Linie und KEIN Text-Label mehr —
// der Chart war zu voll. Es bleibt NUR der Pfeil, RECHTSBÜNDIG am aktuellen
// Rand, auf dem Preisniveau der Wand. Die Details (Seite/Größe/Dominanz)
// stehen im TOOLTIP (Maus drüber) statt fest im Chart.
string dirc = (k > 4) ? p[4] : (isBid ? "U" : "D");
bool isDom = (k > 5 && p[5] == "1");
string arrow = (dirc == "U" ? CharToString(94) : CharToString(118)); // ^ / v
if(isDom)
ObjectSetInteger(0, nm, OBJPROP_WIDTH, 3);
string llbl = arrow + " " + (isBid ? "Liquidität NACHFRAGE " : "Liquidität ANGEBOT ")
+ DoubleToString(lsz, 0) + (isDom ? " <<" : "");
string ltn = PFX + "TLQ_" + side;
if(ObjectCreate(0, ltn, OBJ_TEXT, 0, CenterTime(), lpx))
{
ObjectSetString (0, ltn, OBJPROP_TEXT, llbl);
ObjectSetInteger(0, ltn, OBJPROP_COLOR, lcol);
ObjectSetInteger(0, ltn, OBJPROP_FONTSIZE, 9);
ObjectSetInteger(0, ltn, OBJPROP_ANCHOR, ANCHOR_LOWER);
ObjectSetInteger(0, ltn, OBJPROP_SELECTABLE, false);
}
else
{
ObjectMove(0, ltn, 0, CenterTime(), lpx);
ObjectSetString(0, ltn, OBJPROP_TEXT, llbl);
}
// Echtes Pfeil-Objekt am rechten Rand (Wingdings 233 hoch / 234 runter) —
// deutlicher sichtbar als das Textzeichen im Label.
string an = PFX + "ALQ_" + side;
datetime atime = CenterTime() + (datetime)(PeriodSeconds() * 6);
// Rechter Rand = aktuelle (rechteste) Kerze + kleiner Versatz, damit der
// Pfeil frei neben dem Kursverlauf steht und beim Scrollen rechts bleibt.
datetime atime = iTime(_Symbol, PERIOD_CURRENT, 0)
+ (datetime)(PeriodSeconds() * 2);
if(ObjectCreate(0, an, OBJ_ARROW, 0, atime, lpx))
{
ObjectSetInteger(0, an, OBJPROP_ARROWCODE, (dirc == "U" ? 233 : 234));
ObjectSetInteger(0, an, OBJPROP_COLOR, lcol);
ObjectSetInteger(0, an, OBJPROP_WIDTH, (isDom ? 3 : 2));
ObjectSetInteger(0, an, OBJPROP_WIDTH, (isDom ? 4 : 2));
ObjectSetInteger(0, an, OBJPROP_ANCHOR, ANCHOR_LEFT);
ObjectSetInteger(0, an, OBJPROP_SELECTABLE, false);
}
else
{
ObjectMove(0, an, 0, atime, lpx);
ObjectSetInteger(0, an, OBJPROP_WIDTH, (isDom ? 4 : 2));
ObjectSetInteger(0, an, OBJPROP_COLOR, lcol);
ObjectSetInteger(0, an, OBJPROP_ARROWCODE, (dirc == "U" ? 233 : 234));
}
ObjectSetString(0, an, OBJPROP_TOOLTIP,
(isBid ? "Liquiditaet NACHFRAGE " : "Liquiditaet ANGEBOT ")
+ DoubleToString(lsz, 0) + (isDom ? " (dominant)" : "")
+ " @ " + DoubleToString(lpx, (int)_Digits));
// evtl. noch vorhandene Alt-Objekte (Linie/Label aus v1.13) entfernen
ObjectDelete(0, PFX + "LQ_" + side);
ObjectDelete(0, PFX + "TLQ_" + side);
continue;
}
// Liquiditäts-TRENDLINIE: LT;t1;p1;t2;p2;<U|D> (pink, nach rechts verlängert).
+31 -2
View File
@@ -874,8 +874,37 @@ document.querySelectorAll(".btn[data-side]").forEach(btn => {
if (btn.disabled) return;
const side = btn.dataset.side;
if (side === "long" || side === "short") {
const info = recInfo(side); // WARTEN/Gegen-Signal → Hinweis, der
if (info.danger) { // bestätigt werden muss (kein Block)
// ── Entry-Checkliste (User-Vorgabe 2026-07-30): zeigt VOR der Eröffnung,
// was dagegen spricht — auch dann, wenn ein Signal da ist (der alte Dialog
// kam nur bei WARTEN/Gegen-Signal). Blockt nicht, macht nur sichtbar.
const cl = (lastSnap.checklist || {})[side];
const info = recInfo(side);
if (cl && (cl.fails > 0 || cl.warns > 0)) {
const ICON = { fail: "❌", warn: "⚠", ok: "✅" };
const bad = cl.items.filter(x => x.state !== "ok");
const good = cl.items.filter(x => x.state === "ok");
let body = bad.map(x => `${ICON[x.state]} ${x.label}`
+ (x.detail ? `\n ${x.detail}` : "")).join("\n");
if (good.length) body += `\n\n${good.map(x => `${x.label}`).join("\n")}`;
// Eigene Zahlen zur Ausrichtung ergänzen (gemessene Kern-Leckage)
const al = lastSnap.alignment || null;
const sigNow = (lastSnap.wave_signal || {}).signal || "WARTEN";
const isWait0 = (sigNow === "WARTEN" || sigNow === "—");
if (al && cl.items.some(x => x.key === "signal" && x.state === "fail")) {
const grp = isWait0 ? al.none : al.against, ref = al.with;
const f = g => (g && g.n >= 5)
? `${g.wr}% Treffer · ${g.pnl >= 0 ? "+" : ""}${fmt(g.pnl, 0)} € (n${g.n})` : null;
const own = f(grp), rf = f(ref);
if (own) body += `\n\n📊 Deine Trades ${isWait0 ? "OHNE" : "GEGEN"} Signal: ${own}`;
if (rf) body += `\n📊 Mit Signal: ${rf}`;
}
const head = cl.verdict === "stop"
? `🛑 Checkliste sagt NEIN (${cl.fails} Punkte dagegen)`
: `⚠ Checkliste: ${cl.fails} dagegen, ${cl.warns} Vorsicht`;
const ok = await modal(head, body + `\n\n${side.toUpperCase()} trotzdem eröffnen?`,
"confirm", true);
if (!ok) return;
} else if (info.danger) { // Fallback: alter Hinweis (kein Block)
const sig = (lastSnap.wave_signal || {}).signal || "WARTEN";
const isWait = (sig === "WARTEN" || sig === "—");
const head = isWait
+2 -2
View File
@@ -6,7 +6,7 @@
<meta name="theme-color" content="#0d1117">
<title>Oil · MT5</title>
<link rel="manifest" href="manifest.json">
<link rel="stylesheet" href="style.css?v=119">
<link rel="stylesheet" href="style.css?v=120">
</head>
<body>
<!-- Modul-Menü (ganz oben) -->
@@ -229,6 +229,6 @@
<div id="toast" class="toast hidden"></div>
<script src="lightweight-charts.standalone.production.js?v=114"></script>
<script src="app.js?v=119"></script>
<script src="app.js?v=120"></script>
</body>
</html>