M15-Karte: Bias war ein Richtungsgeber - korrigiert zum Veto (v=155)

Tagestest an 07.08. (test_m15_karte_tag.py) deckt einen strukturellen Fehler in
meiner eigenen Karte auf.

Rohzahlen: 92 M15-Bars, davon 46 (50 %) auf GRUEN. Entkoppelt auf
zusammenhaengende Bloecke bleiben 13 Setup-Episoden - und ALLE 13 waren LONG,
an einem Tag, der von 78,399 auf 77,383 FIEL (-1,016 $). Trefferquote 0 %,
OeR -0,753, SumR -9,79.

Ursache ist nicht Pech, sondern ein Denkfehler: gemessen ist "gegen den
M30-Trend zu handeln halbiert den Edge" - eine VERBOTS-Aussage. Ich hatte
daraus "M30+H1 einig -> Bias LONG" gemacht, also einen Richtungsgeber. Exakt
der Fehler, wegen dem die Gesamtempfehlung entfernt wurde (Richtung dort 48-51 %
Treffer). Die nachlaufende EMA zeigt im Trendtag stur in die alte Richtung -
dokumentiert seit analyze_signal_live.py.

Korrigiert: das Feld liefert nur noch, welche Richtung VERBOTEN ist
(bias.verboten); bias.richtung ist dauerhaft None. Das Banner nennt keine
Richtung mehr:
  "BEDINGUNGEN OK @ 77,098 - Order ANS LEVEL, nicht zum Markt - SHORT vermeiden"

Zwei weitere Befunde aus demselben Test, offen dokumentiert:
- Das Kosten-Gate blockte 0 von 92 Bars. Auf M15 liegt Spread/ATR bei ~0,10,
  die Schwelle bei 0,32 - das Gate ist dort faktisch dekorativ.
- Die strengere Bias-Variante (M30 UND H1) unterscheidet sich kaum von der
  laxen (46 gegen 47 gruene Bars) - die Zusatzstrenge kostet nichts, bringt
  aber auch nichts.

Auch ein Fehler in meinem TEST, dokumentiert: der erste Lauf zaehlte 46
fortlaufende Bars als 46 Trades und meldete 7 % Trefferquote - eine Bewegung
46-mal gezaehlt. Erst die Entkopplung auf Episoden macht die Zahl lesbar.

WARNUNG zur Beweiskraft: EIN Tag ist kein Edge-Nachweis. Belastbar ist hier
nicht die Trefferquote, sondern der MECHANISMUS - und der ist aus zwei
Halbjahren dokumentiert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-08 23:14:47 +02:00
co-authored by Claude Opus 5
parent e5adfc9757
commit 30830d76b1
4 changed files with 232 additions and 16 deletions
+20 -4
View File
@@ -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 4851 % 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).
+193
View File
@@ -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())
+17 -10
View File
@@ -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;
+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=154">
<link rel="stylesheet" href="style.css?v=155">
</head>
<body>
<!-- Modul-Menü (ganz oben) -->
@@ -340,6 +340,6 @@
</div>
<div id="toast" class="toast hidden"></div>
<script src="app.js?v=154"></script>
<script src="app.js?v=155"></script>
</body>
</html>