#!/usr/bin/env python3 """Sagt das KERZENMUSTER die Richtung der naechsten Kerze? (User 2026-08-17) Vorlauf: `backtest_naechste_kerze.py` hat gezeigt, dass die naechste M5-Kerze grundsaetzlich nur ueber 1-Bar-Mean-Reversion schwach vorhersagbar ist (51,3 %), und dass der Effekt 50-100x unter dem Spread liegt. Dort wurden aber nur GROBE Proxys geprueft (letzte Kerze fort/umkehr, langer Docht). Hier die richtige Frage: die echten MUSTER-Klassen — mit denselben Schwellen wie der Live-Detektor `core/candles.py` (Marubozu, langer Koerper, langer Docht oben/unten, Spinning Top, Doji, Engulfing) plus die Zwei-Bar-Muster Inside/Outside Bar, die dort nicht klassifiziert werden. ⚠ WAS BEREITS GEMESSEN IST (und was hier NEU ist): `backtest_candles.py` (31.07.) hat die Muster gegen den Forward-Return ueber **60 Minuten** gemessen — Ergebnis: die Lehrbuch-Lesart ist in BEIDEN Haelften INVERTIERT („langer Koerper = Fortsetzung" -0,059/-0,076; „2x Volumen = echt" -0,117/-0,204, monoton falsch herum). `backtest_candle_fade.py` und `_fade2` haben den Fade als HANDELSREGEL zweimal verworfen (zuletzt alle 40 Zellen negativ). NEU ist allein der **1-Bar-Horizont** — die Frage des Users. VORAB FIXIERTE REGEL: ein Muster taugt, wenn es (1) in BEIDEN Haelften > 50 % trifft, (2) beide 95-%-Intervalle die 50 ausschliessen, (3) n >= 300 je Haelfte, und (4) die Oe-Bewegung in Vorhersagerichtung den HALBEN Spread (0,0100 $) schlaegt. ⚠ (4) ist die harte Huerde: eine M5-Kerze bewegt sich im Median 0,0400 $, der Spread ist 0,0200 $ = 50 % davon. ⚠ UNVERAENDERTE Schlusskurse werden AUSGESCHLOSSEN (~11 % der Kerzen). Sie als Fehltreffer zu zaehlen liess im Vorlauf ein Muster UND sein Gegenteil beide unter 50 % liegen — arithmetisch unmoeglich, und der Verraeter des Fehlers. Aufruf: python backtest_kerzenmuster_richtung.py [bars] """ from __future__ import annotations import random import sys import MetaTrader5 as mt5 # Schwellen 1:1 aus dem Live-Detektor from core.candles import (_DOJI_BR, _LONG_BR, _LONG_X, _MARU_BR, _REF_N, _SPIN_BR, _SPIN_W, _VOL_HOT, _WICK) def muster(O, H, L, C, V, i): """Klassifiziert Bar i — gibt (Name, Richtungs-Erwartung) zurueck. Die Richtungs-Erwartung ist die LEHRBUCH-Lesart (+1 aufwaerts / -1 abwaerts), damit sichtbar wird, ob sie stimmt oder — wie ueber 60 min gemessen — invertiert ist. `0` = Muster ohne Richtungsaussage. """ sp = H[i] - L[i] if sp <= 0: return None body = C[i] - O[i] ab = abs(body) br = ab / sp ob = H[i] - max(O[i], C[i]) # oberer Docht un = min(O[i], C[i]) - L[i] # unterer Docht d = 1 if body > 0 else -1 lo = max(0, i - _REF_N) med_body = sorted(abs(C[j] - O[j]) for j in range(lo, i)) or [0] med = med_body[len(med_body) // 2] if med_body else 0 lang = med > 0 and ab >= _LONG_X * med # Zwei-Bar-Muster zuerst (spezifischer) if H[i] <= H[i - 1] and L[i] >= L[i - 1]: return ("Inside Bar", 0) if H[i] > H[i - 1] and L[i] < L[i - 1]: return ("Outside Bar", d) vor = abs(C[i - 1] - O[i - 1]) if ab > vor > 0 and (C[i] - O[i]) * (C[i - 1] - O[i - 1]) < 0 \ and max(O[i], C[i]) >= max(O[i - 1], C[i - 1]) \ and min(O[i], C[i]) <= min(O[i - 1], C[i - 1]): return ("Engulfing", d) # Lehrbuch: in Richtung der Huellkerze if br >= _MARU_BR: return ("Marubozu", d) if br >= _LONG_BR and lang: return ("langer Koerper", d) # Lehrbuch: Fortsetzung if br <= _DOJI_BR: return ("Doji", 0) if br <= _SPIN_BR and ob > _SPIN_W * sp and un > _SPIN_W * sp: return ("Spinning Top", 0) if ob / sp >= _WICK: return ("langer Docht OBEN", -1) # Lehrbuch: abwaerts if un / sp >= _WICK: return ("langer Docht UNTEN", +1) # Lehrbuch: aufwaerts return None def ci(tr, n, seed=5, w=2000): if n < 50: return None, None r = random.Random(seed); p = tr / n b = sorted(sum(1 if r.random() < p else 0 for _ in range(n)) / n for _ in range(w)) return 100 * b[int(.025 * w)], 100 * b[int(.975 * w)] def main(): n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000 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_M5, 0, n) point = mt5.symbol_info(sym).point mt5.shutdown() O = [float(b["open"]) 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] V = [float(b["tick_volume"]) for b in bars] SP = sorted(float(b["spread"]) * point for b in bars) med_sp = SP[len(SP) // 2] mid = len(C) // 2 print(f"\n {sym} · {len(C)} M5-Bars · Muster-Schwellen 1:1 aus core/candles.py") print(f" Huerde: halber Spread {med_sp/2:.4f} $ — so viel muss die Ø-Bewegung") print(f" in Vorhersagerichtung mindestens bringen.\n") print(f" {'Muster':<22}{'Lehrb.':>7}{'Haelfte':>8}{'n':>7}{'Treffer':>9}" f"{'95-%-KI':>16}{'Ø Bewegung':>13}") # sammeln daten: dict = {} for i in range(_REF_N + 2, len(C) - 1): m = muster(O, H, L, C, V, i) if not m: continue name, erw = m if erw == 0: continue # ohne Richtungsaussage nicht bewertbar bew = (C[i + 1] - C[i]) * erw if bew == 0: continue # unveraenderter Schluss — ausgeschlossen daten.setdefault(name, {"H1": [], "H2": []}) daten[name]["H1" if i < mid else "H2"].append(bew) besteht = [] for name in sorted(daten): zeilen = [] for lbl in ("H1", "H2"): w = daten[name][lbl] if len(w) < 50: zeilen.append(None); continue tr = sum(1 for x in w if x > 0) q = 100 * tr / len(w) u, o = ci(tr, len(w)) mb = sum(w) / len(w) flag = "" if u is None or (u <= 50 <= o) else (" ✅" if u > 50 else " ❌") print(f" {name:<22}{'±1':>7}{lbl:>8}{len(w):>7}{q:>8.1f}%" f" [{u:>4.1f} … {o:>4.1f}]{mb:>+11.4f} $" + flag) zeilen.append((len(w), q, u, o, mb)) if all(z and z[0] >= 300 and z[2] > 50 for z in zeilen): besteht.append((name, zeilen)) print() print(" ── URTEIL ─────────────────────────────────────────────────────") if not besteht: print(" KEIN Muster erfuellt (1)-(3) in beiden Haelften.") for name, z in besteht: gross = all(abs(x[4]) > med_sp / 2 for x in z) print(f" {name}: Trefferquote besteht — Bewegung " f"{'SCHLAEGT' if gross else 'VERFEHLT'} den halben Spread " f"({z[0][4]:+.4f} / {z[1][4]:+.4f} $ gegen {med_sp/2:.4f})") print("\n ⚠ Eine Trefferquote ueber 50 % ist NICHT genug — sie muss die") print(" Kosten tragen. Genau daran ist der 1-Bar-Effekt im Vorlauf") print(" gescheitert (51,3 % Treffer, aber ±0,0002 $ Bewegung).") return 0 if __name__ == "__main__": raise SystemExit(main())