#!/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())
