diff --git a/core/engine.py b/core/engine.py index b504b8f..0f6acbd 100644 --- a/core/engine.py +++ b/core/engine.py @@ -4034,12 +4034,28 @@ class TradingEngine: atr15 = (ws.get("tf_atr") or {}).get("M15") out: dict = {"atr_m15": round(atr15, 4) if atr15 else None} - # ── 1) BIAS — der einzige belegte Filter (M30-Gegen-Trend: Edge ×2) + # ── 1) VETO statt BIAS ──────────────────────────────────────── + # ⚠⚠ KORREKTUR 2026-08-08, nach dem Tagestest an 07.08.: der erste + # Entwurf machte aus „M30+H1 einig" eine RICHTUNG („Bias LONG") und + # erzeugte damit 13 LONG-Episoden an einem Tag, der 1,02 $ FIEL — + # Trefferquote 0 %, ØR −0,753. Der Fehler ist strukturell, nicht + # Pech: gemessen ist **„gegen den M30-Trend halbiert den Edge"**, + # also eine VERBOTS-Aussage. Daraus einen Richtungsgeber zu machen + # ist exakt der Fehler, wegen dem die Gesamtempfehlung entfernt + # wurde (Richtung dort 48–51 % Treffer = Münzwurf). + # Die nachlaufende EMA zeigt im Trendtag stur in die alte Richtung — + # dokumentiert seit `analyze_signal_live.py` (06.08.). + # Jetzt liefert das Feld nur noch, welche Richtung VERBOTEN ist. h30 = ws.get("htf_trend"); h1 = ws.get("h1_trend") - einig = (h30 and h1 and h30 == h1) + verboten = None + if h30: + verboten = "SHORT" if h30 == "LONG" or h30 == 1 else "LONG" out["bias"] = {"m30": h30, "h1": h1, - "richtung": h30 if einig else None, - "einig": bool(einig)} + "verboten": verboten, + "einig": bool(h30 and h1 and h30 == h1), + # ⚠ `richtung` bleibt None — bewusst KEINE + # Richtungsaussage mehr (s. o.). + "richtung": None} # ── 2) KOSTEN auf M15 — hier gewinnt M15 wirklich (gemessen 0,104 # gegen M5 0,193). Schwellen wie beim M5-Chip (0,20 / 0,32). diff --git a/test_m15_karte_tag.py b/test_m15_karte_tag.py new file mode 100644 index 0000000..d26de9d --- /dev/null +++ b/test_m15_karte_tag.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +"""Plausibilitaets-Test der M15-Karte an EINEM Handelstag. + +⚠⚠ WAS DAS IST UND WAS NICHT. Ein Tag ist KEIN Edge-Nachweis — die +2-Stichproben-Regel gilt unveraendert, und im Projekt sind Einzeltag-Anekdoten +schon zweimal als Beleg missbraucht worden (15-Minuten-Regel 31.07., +Nacht-Guard 05.08.). Geprueft wird deshalb nur die MECHANIK: + · Wie oft steht die Karte auf gruen / gelb / rot? + · Sind die Gates zu streng (nie gruen) oder zu lasch (dauernd gruen)? + · WELCHES Gate blockt wie oft — dieselbe Telemetrie-Frage wie bei + `block_reason`, die den 93-%-WARTEN-Befund erst erklaerbar gemacht hat. + · Und als Kontext, nicht als Beleg: was waere nach einem gruenen Status + passiert? + +⚠ ZWEI SCHWELLEN SIND GESETZT, NICHT GEMESSEN — und der Test soll das sichtbar +machen, nicht verstecken: + (a) „Level in Reichweite" = 0,5×ATR_M15 — von mir gewaehlt. + (b) „Bias einig" verlangt M30 UND H1 in derselben Richtung. Gemessen ist nur + der M30-GEGEN-Trend-Filter (Edge ×2); H1 ist im Projekt Konfluenz-BONUS, + kein Pflicht-Gate. Die Und-Verknuepfung ist strenger als alles Belegte. +Beide Varianten werden deshalb mitgerechnet, damit der Preis der Strenge +sichtbar wird. + +Aufruf: python test_m15_karte_tag.py [YYYY-MM-DD] (Default 2026-08-07) +""" +from __future__ import annotations + +import sys +from datetime import datetime, timedelta, timezone +from zoneinfo import ZoneInfo + +import MetaTrader5 as mt5 + +from core.exit_model import LIVE, simulate +from core.sr_levels import halten, linien, naechste + +_BERLIN = ZoneInfo("Europe/Berlin") +_BROKER = timezone(timedelta(hours=3)) +_HTF_DEAD = 0.15 # Totband wie _HTF_DEADBAND +_PIV_K = 3 +_LOOK = 50 # M30-Pivot-Fenster wie live + + +def _berlin(ts): + return datetime.fromtimestamp(ts, timezone.utc).replace( + tzinfo=_BROKER).astimezone(_BERLIN) + + +def ema(v, p): + k = 2 / (p + 1); out = [v[0]] + for x in v[1:]: + out.append(out[-1] + k * (x - out[-1])) + return out + + +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] + for i in range(1, len(C)): + seg = tr[max(1, i - p + 1):i + 1] + out.append(sum(seg) / len(seg)) + return out + + +def htf_sign(H, L, C, step): + """Trend-Vorzeichen der hoeheren TF je M15-Index — LOOK-AHEAD-frei.""" + hh, hl, hc = [], [], [] + for s in range(0, len(C) - step + 1, step): + hh.append(max(H[s:s + step])); hl.append(min(L[s:s + step])) + hc.append(C[s + step - 1]) + if len(hc) < 55: + return [0] * len(C) + ef, es = ema(hc, 12), ema(hc, 50) + ha = atr_series(hh, hl, hc) + sg = [] + for j in range(len(hc)): + a = ha[j] + if j < 52 or not a: + sg.append(0); continue + d = ef[j] - es[j] + sg.append(0 if abs(d) < _HTF_DEAD * a else (1 if d > 0 else -1)) + out = [0] * len(C) + for i in range(len(C)): + j = i // step - 1 + out[i] = sg[j] if 0 <= j < len(sg) else 0 + return out + + +def m30_levels(H, L, C): + """Rohe M30-Pivots je M15-Index (2 M15-Bars = 1 M30-Bar), kausal.""" + st = 2 + mh = [max(H[s:s + st]) for s in range(0, len(C) - st + 1, st)] + ml = [min(L[s:s + st]) for s in range(0, len(C) - st + 1, st)] + out = [None] * len(C) + for i in range(len(C)): + j = i // st - 1 + if j < _PIV_K + 5: + continue + a, b = max(0, j - _LOOK), j + 1 + hs, ls = mh[a:b], ml[a:b] + ph = [round(hs[x], 3) for x in range(_PIV_K, len(hs) - _PIV_K) + if hs[x] == max(hs[x - _PIV_K:x + _PIV_K + 1])] + pl = [round(ls[x], 3) for x in range(_PIV_K, len(ls) - _PIV_K) + if ls[x] == min(ls[x - _PIV_K:x + _PIV_K + 1])] + out[i] = (ph, pl) + return out + + +def main(): + tag = sys.argv[1] if len(sys.argv) > 1 else "2026-08-07" + mt5.initialize() + sym = next((c for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD") + if mt5.symbol_info(c)), None) + bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M15, 0, 4000) + point = mt5.symbol_info(sym).point + mt5.shutdown() + T = [int(b["time"]) for b in bars] + 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) + S30, S1H = htf_sign(H, L, C, 2), htf_sign(H, L, C, 4) + LV = m30_levels(H, L, C) + + idx = [i for i, t in enumerate(T) if _berlin(t).strftime("%Y-%m-%d") == tag] + if not idx: + print(f"keine M15-Bars fuer {tag}"); return 1 + + print("=" * 92) + print(f" M15-KARTE — Mechanik-Test an {tag} ({sym}, {len(idx)} M15-Bars)") + print(" ⚠ EIN Tag = kein Edge-Nachweis. Geprueft wird nur, ob die Gates") + print(" plausibel greifen (zu streng / zu lasch) und welches blockt.") + print("=" * 92) + + zaehler = {"gruen": 0, "gelb": 0, "rot_bias": 0, "rot_kosten": 0, "kein_level": 0} + zaehler_lax = dict(zaehler) # Variante: nur „nicht gegen M30" + gruene = [] + prev_r = prev_s = None + for i in idx: + a = A[i] + if not a or not LV[i]: + continue + ph, pl = LV[i] + lv = linien(ph, pl, a) + r, s = naechste(lv, C[i]) + r = halten(r, prev_r, C[i], a, True); s = halten(s, prev_s, C[i], a, False) + prev_r, prev_s = r, s + kr = SP[i] / a + kOk = kr < 0.32 + gaps = [abs(x - C[i]) / a for x in (r, s) if x is not None] + nah = [g for g in gaps if g <= 0.5] + + for name, bOk, d in (("streng", S30[i] != 0 and S30[i] == S1H[i], S30[i]), + ("lax", S30[i] != 0, S30[i])): + z = zaehler if name == "streng" else zaehler_lax + if not bOk: z["rot_bias"] += 1 + elif not kOk: z["rot_kosten"] += 1 + elif not gaps: z["kein_level"] += 1 + elif not nah: z["gelb"] += 1 + else: + z["gruen"] += 1 + if name == "streng": + lvl = min((x for x in (r, s) if x is not None), + key=lambda x: abs(x - C[i])) + gruene.append((i, d, lvl, a, kr)) + + n = len(idx) + print(f"\n {'Status':<16}{'STRENG (M30 UND H1)':>22}{'LAX (nur nicht gegen M30)':>28}") + for k, lbl in (("gruen", "🟢 SETUP BEREIT"), ("gelb", "🟡 warten auf Level"), + ("rot_bias", "🔴 Bias"), ("rot_kosten", "🔴 Kosten"), + ("kein_level", "kein Level")): + print(f" {lbl:<16}{zaehler[k]:>10} ({100*zaehler[k]/n:>4.0f} %)" + f"{zaehler_lax[k]:>16} ({100*zaehler_lax[k]/n:>4.0f} %)") + + print(f"\n ── Was folgte auf 🟢? (KONTEXT, kein Beleg — n={len(gruene)}) ──") + if not gruene: + print(" kein einziges gruenes Signal an diesem Tag.") + else: + Rs = [] + for i, d, lvl, a, kr in gruene: + R = simulate(lvl, d, a, H, L, C, i + 1, LIVE) + Rs.append(R - kr) + w = sum(1 for x in Rs if x > 0) + print(f" Trefferquote {100*w/len(Rs):.0f} % ØR {sum(Rs)/len(Rs):+.3f} " + f"ΣR {sum(Rs):+.2f}") + print(f" Zeiten: " + ", ".join(f"{_berlin(T[i]):%H:%M}" for i, *_ in gruene[:12])) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/web/app.js b/web/app.js index 49c4fb7..207064d 100644 --- a/web/app.js +++ b/web/app.js @@ -159,12 +159,16 @@ function renderM15(s) { const f = (v, d = 2) => (v == null ? "—" : (+v).toFixed(d)); // ── Gate 1: HTF-Bias (der einzige belegte Filter, Edge ×2) ─────────── + // ⚠⚠ VETO, KEINE RICHTUNG (Korrektur 2026-08-08). Der erste Entwurf zeigte + // „Bias ▲ LONG" und erzeugte am 07.08. dreizehn LONG-Episoden an einem Tag, + // der 1,02 $ FIEL (Trefferquote 0 %). Gemessen ist nur: GEGEN den M30-Trend + // zu handeln halbiert den Edge — eine Verbots-, keine Richtungsaussage. const b = s.bias || {}; - const bOk = !!b.einig; - $("m15-bias").className = "m15-item " + (bOk ? "ok" : "fail"); + const bOk = !!b.verboten; // Gate offen, sobald ein HTF steht + $("m15-bias").className = "m15-item " + (bOk ? "ok" : ""); $("m15-bias").textContent = bOk - ? `① Bias ${b.richtung === "LONG" ? "▲ LONG" : "▼ SHORT"} · M30+H1 einig` - : `① Bias unklar · M30 ${b.m30 || "—"} / H1 ${b.h1 || "—"}`; + ? `① Veto: ${b.verboten} vermeiden (HTF ${b.m30}${b.einig ? " + H1 einig" : ""})` + : `① kein Veto — HTF flach (M30 ${b.m30 ?? "—"} / H1 ${b.h1 ?? "—"})`; // ── Gate 2: Kosten auf M15 ─────────────────────────────────────────── const k = s.kosten || {}; @@ -224,15 +228,18 @@ function renderM15(s) { : "⑤ Veto: noch zu wenige Trades"; // ── Status: die Kette wird der Reihe nach ausgewertet ──────────────── + // ⚠ Das Banner nennt AUSDRUECKLICH keine Richtung. Es sagt, ob die + // BEDINGUNGEN fuer eine Order am Level stehen — und was verboten ist. let cls, txt; - if (!bOk) { cls = "rot"; txt = "🔴 KEIN TRADE · Bias unklar (M30/H1 uneinig)"; } - else if (!kOk && k.ratio != null) { cls = "rot"; txt = "🔴 KEIN TRADE · Kosten zu hoch"; } - else if (!zOk) { cls = "gelb"; txt = "🟡 WARTEN AUF LEVEL · Bias und Kosten passen"; } - else { + if (!kOk && k.ratio != null) { cls = "rot"; txt = "🔴 KEIN TRADE · Kosten zu hoch"; } + else if (!zOk) { + cls = "gelb"; + txt = "🟡 WARTEN AUF LEVEL · Kosten passen, Kurs noch zu weit"; + } else { const nn = nah[0]; cls = "gruen"; - txt = `🟢 SETUP BEREIT @ ${f(nn.level, 3)} · ${b.richtung === "LONG" ? "▲" : "▼"} ` - + `${b.richtung} · Order ANS LEVEL legen, nicht zum Markt`; + txt = `🟢 BEDINGUNGEN OK @ ${f(nn.level, 3)} · Order ANS LEVEL, nicht zum Markt` + + (b.verboten ? ` · ⛔ ${b.verboten} vermeiden` : ""); } st.className = "m15-status " + cls; st.textContent = txt; diff --git a/web/index.html b/web/index.html index 61c27fd..aef08c9 100644 --- a/web/index.html +++ b/web/index.html @@ -6,7 +6,7 @@