#!/usr/bin/env python3
"""Halten Fibonacci-Retracements? (2026-08-04)

Anlass: Kommentar zu USOIL — „Ein Fibonacci-Retracement sollte den Kurs vor dem
nächsten Aufwärtsimpuls auf 79 $ drücken." Fibonacci-Level sind in diesem Projekt
**nie gemessen worden**, weder als S/R noch als Einstiegszone. Diese Lücke wird
hier geschlossen.

⚠ Ausgangslage ist zwiespältig, und das ist der Grund für die Kontrollgruppen:
`backtest_structure_tf.py` hat gezeigt, dass **gerechnete Linien** (Regressions-
kanal) NICHT halten — 24/24 Zellen negativ, weil hinter einer gerechneten Linie
keine Orders liegen. Ein Fib-Level ist ebenfalls gerechnet. **ABER** Fibonacci
wird von sehr vielen Marktteilnehmern beobachtet — das Selbsterfüllungs-Argument,
das beim Regressionskanal fehlt. Ob es trägt, entscheidet der Vergleich gegen:
  (a) ZUFALLS-Level im selben Abstand  → misst „ist Fib besser als irgendein Level?"
  (b) M30-PIVOTS (was der Bot heute nutzt) → misst „ist Fib besser als Struktur?"

Zwei Teile:
  TEIL A — HALTEQUOTE: prallt der Kurs an einem Fib-Level häufiger ab als an den
           Kontroll-Leveln? Barriere-Rennen wie beim P(break)-Training
           (±0,5×ATR innerhalb von 12 M5-Bars, Timeout zählt als Abprall).
  TEIL B — HANDELBARKEIT: Bounce-Einstieg am Level, kanonischer Live-Exit,
           Echtkosten, 2 Halbjahre.

Schwünge stammen aus **M15**-Pivots (die im Kommentar genannte Zeitebene),
Berührungen und Trades laufen auf **M5** (dort ist der Live-Exit kalibriert).

⚠ Regel VORAB fixiert: Fib gilt nur als tragfähig, wenn die Haltequote in BEIDEN
Hälften über BEIDEN Kontrollen liegt UND Teil B in beiden Hälften ØR>0 mit PF>1
liefert. Ein einzelner besserer Wert zählt nicht.

Aufruf:  python backtest_fibonacci.py [bars]
"""
import random
import sys

import MetaTrader5 as mt5

from core.analysis import FIB_RATIOS
from core.exit_model import LIVE, simulate

_ATRMIN = 0.06
_TOUCH = 0.15          # Berührung: |Kurs − Level| ≤ x×ATR (wie P(break)-Training)
_BARRIER = 0.5         # Ausgang: ±x×ATR (wie P(break)-Training)
_WIN = 12              # Fenster in M5-Bars (= 60 min, wie P(break)-Training)
_PIVK = 3              # Pivot-Fenster
_MIN_SWING_ATR = 2.0   # Schwung muss mind. so groß sein, sonst kein Retracement
_COOL = 6


def _atr_series(H, L, C, p=14):
    t = [0.0]
    for i in range(1, len(C)):
        t.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1])))
    return [(sum(t[max(1, i - p + 1):i + 1]) / max(1, len(t[max(1, i - p + 1):i + 1])))
            if i else None for i in range(len(C))]


def _m15_swings(H, L, n):
    """M15-Pivots (k=3) → Liste (bekannt_ab_m5_index, tief, hoch, richtung).

    Kausal: ein M15-Pivot bei Bar j ist erst bei j+_PIVK bestätigt; der
    entsprechende M5-Index ist (j+_PIVK)*3.
    """
    HH = [max(H[i:i + 3]) for i in range(0, n - 2, 3)]
    LL = [min(L[i:i + 3]) for i in range(0, n - 2, 3)]
    piv = []
    for j in range(_PIVK, len(HH) - _PIVK):
        if HH[j] == max(HH[j - _PIVK:j + _PIVK + 1]):
            piv.append((j, HH[j], "H"))
        elif LL[j] == min(LL[j - _PIVK:j + _PIVK + 1]):
            piv.append((j, LL[j], "L"))
    swings = []
    for a, b in zip(piv, piv[1:]):
        if a[2] == b[2]:
            continue
        bekannt = (b[0] + _PIVK) * 3           # erst dann im M5-Index nutzbar
        if a[2] == "L":
            swings.append((bekannt, a[1], b[1], +1))   # Aufwärts-Schwung
        else:
            swings.append((bekannt, b[1], a[1], -1))   # Abwärts-Schwung
    return swings


def _aktiver_swing(swings, idx, i):
    """Letzter Schwung, der bei M5-Bar i bereits bekannt ist."""
    while idx + 1 < len(swings) and swings[idx + 1][0] <= i:
        idx += 1
    return idx


def _ausgang(H, L, i, level, d_app, atr):
    """Barriere-Rennen. d_app = Richtung, aus der der Kurs kam (+1 = von oben).
    → 'hold' (prallt zurück) oder 'break' (läuft durch). Timeout = hold."""
    zurueck = level + d_app * _BARRIER * atr
    durch = level - d_app * _BARRIER * atr
    for j in range(i + 1, min(i + 1 + _WIN, len(H))):
        if d_app > 0:
            if H[j] >= zurueck:
                return "hold"
            if L[j] <= durch:
                return "break"
        else:
            if L[j] <= zurueck:
                return "hold"
            if H[j] >= durch:
                return "break"
    return "hold"


def _quote(res):
    n = len(res)
    return (n, 100.0 * sum(1 for x in res if x == "hold") / n) if n else (0, 0.0)


def main():
    n_req = int(sys.argv[1]) if len(sys.argv) > 1 else 80000
    random.seed(20260804)
    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_req)
    point = mt5.symbol_info(sym).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)
    N = len(C); mid = N // 2
    swings = _m15_swings(H, L, N)
    # M30-Pivots als Kontrolle (b) — dieselbe Quelle, die der Bot live nutzt
    HH30 = [max(H[i:i + 6]) for i in range(0, N - 5, 6)]
    LL30 = [min(L[i:i + 6]) for i in range(0, N - 5, 6)]
    p30 = []
    for j in range(_PIVK, len(HH30) - _PIVK):
        if HH30[j] == max(HH30[j - _PIVK:j + _PIVK + 1]):
            p30.append(((j + _PIVK) * 6, HH30[j]))
        elif LL30[j] == min(LL30[j - _PIVK:j + _PIVK + 1]):
            p30.append(((j + _PIVK) * 6, LL30[j]))

    print("=" * 94)
    print(f"  FIBONACCI-RETRACEMENTS — {sym} · Schwünge aus M15, Berührung/Trade auf M5")
    print(f"  {N} Bars · 2 Halbjahre · Berührung ≤{_TOUCH}×ATR · Ausgang ±{_BARRIER}×ATR "
          f"in {_WIN} Bars")
    print(f"  Kontrollen: (a) Zufalls-Level gleicher Distanz  (b) M30-Pivots (Live-Quelle)")
    print("=" * 94)

    for lbl, lo_i, hi_i in (("H1 (alt)", 0, mid), ("H2 (neu)", mid, N - _WIN - 2)):
        res_fib, res_rnd, res_piv = [], [], []
        trades = []
        letzte = {}
        si = 0; pi = 0
        i = max(lo_i, 200)
        while i < hi_i:
            atr = A[i]
            if not atr or atr < _ATRMIN:
                i += 1; continue
            si = _aktiver_swing(swings, si, i)
            if si >= len(swings) or swings[si][0] > i:
                i += 1; continue
            _, slo, shi, sdir = swings[si]
            if (shi - slo) < _MIN_SWING_ATR * atr:
                i += 1; continue
            while pi + 1 < len(p30) and p30[pi + 1][0] <= i:
                pi += 1
            # --- Fib-Level des aktiven Schwungs
            for r in FIB_RATIOS:
                lvl = shi - (shi - slo) * r if sdir > 0 else slo + (shi - slo) * r
                if abs(C[i] - lvl) > _TOUCH * atr:
                    continue
                key = ("fib", round(lvl, 2))
                if i - letzte.get(key, -999) < _COOL:
                    continue
                letzte[key] = i
                d_app = 1 if C[i - 1] > lvl else -1
                res_fib.append(_ausgang(H, L, i, lvl, d_app, atr))
                # TEIL B: Bounce-Trade zurück in die Anlaufrichtung
                d = d_app
                cost = (SP[i] if SP[i] > 0 else 0.0225) / atr
                r_, _xj = simulate(C[i], d, atr, H, L, C, i + 1, LIVE,
                                   timestop=True, use_tp=True, ret_bar=True)
                trades.append(r_ - cost)
            # --- Kontrolle (a): Zufalls-Level im selben Abstandsbereich
            lvl_r = C[i] + random.uniform(-_TOUCH, _TOUCH) * atr
            key = ("rnd", i // _COOL)
            if key not in letzte:
                letzte[key] = i
                d_app = 1 if C[i - 1] > lvl_r else -1
                res_rnd.append(_ausgang(H, L, i, lvl_r, d_app, atr))
            # --- Kontrolle (b): M30-Pivot
            if pi < len(p30):
                lvl_p = p30[pi][1]
                if abs(C[i] - lvl_p) <= _TOUCH * atr:
                    key = ("piv", round(lvl_p, 2))
                    if i - letzte.get(key, -999) >= _COOL:
                        letzte[key] = i
                        d_app = 1 if C[i - 1] > lvl_p else -1
                        res_piv.append(_ausgang(H, L, i, lvl_p, d_app, atr))
            i += 1

        print(f"\n  {lbl}")
        for name, res in (("Fibonacci-Level", res_fib),
                          ("(a) Zufalls-Level", res_rnd),
                          ("(b) M30-Pivots", res_piv)):
            c, q = _quote(res)
            print(f"     {name:<20} n={c:>6}   Haltequote {q:5.1f} %")
        if trades:
            nt = len(trades); s = sum(trades)
            up = sum(x for x in trades if x > 0); dn = -sum(x for x in trades if x < 0)
            print(f"     TEIL B Bounce-Trade  n={nt:>6}   WR "
                  f"{100*sum(1 for x in trades if x>0)/nt:4.1f} %  ØR {s/nt:+.3f}  "
                  f"PF {(up/dn if dn else 9.99):4.2f}  ΣR {s:+.0f}")
    print("\n  Regel (vorab): tragfähig nur, wenn die Haltequote in BEIDEN Hälften über")
    print("  BEIDEN Kontrollen liegt UND Teil B beidhälftig ØR>0 mit PF>1 liefert.")
    print()


if __name__ == "__main__":
    main()
