#!/usr/bin/env python3
"""Zieht eine EROEFFNUNGSLUECKE den Kurs an? — die nie gemessene Gap-Klasse.

ANLASS (2026-08-11, User: „wir haben ja eine Kursluecke. Vielleicht will der Kurs
erst diese schliessen?"). Am 10.08. oeffnete WTI bei 78,769 nach einem
Freitagsschluss von 77,383 — eine offene `close_open`-Luecke von 1,386 $, deren
Fuell-Level UNTER dem Kurs liegt. Die Frage ist also handelsrelevant: sie stuetzt
oder widerlegt eine offene SHORT-Position.

⚠⚠ WARUM DAS NICHT SCHON BEANTWORTET IST. Das Projekt hat den „Gap-Magneten"
gemessen und VERWORFEN (`backtest_gaps.py`: Signale WEG vom Gap +0,094 Ø-Edge,
ZUM Gap nur +0,014). Dieser Befund beruht aber auf der **`vacuum`**-Definition —
dem Vakuum zwischen zwei Tagesspannen. `core/gaps.py` sagt das ausdruecklich:
`zone_lines()` speist NUR `vacuum` in die Strategie, „denn nur auf ihnen wurde
gemessen … wer `close_open` einbeziehen will, misst vorher."
**Genau das passiert hier.** Die Klasse, die ein Mensch „Kursluecke" nennt
(Schluss → Eroeffnung, typisch die Wochenendluecke), ist ungemessen.

METHODE — und der Kontrolltest ist der entscheidende Teil:
  · Luecken exakt wie `core/gaps.py` (kein Vakuum, |Schluss→Open| >= min).
  · FUELLQUOTE: erreicht der Kurs binnen N Handelstagen den Vortags-Schluss?
  · ⚠⚠ KONTROLLE (A) — **dieselbe Distanz, dieselbe Bar, GEGENrichtung.**
    Fuer eine Aufwaerts-Luecke verlangt die Fuellung einen Ruecklauf um `size`.
    Die Kontrolle fragt: laeuft der Kurs im selben Fenster `size` nach OBEN?
    Damit sind Volatilitaet, Regime und Zeitraum automatisch gleich — es bleibt
    nur die Frage, ob die Luecke eine RICHTUNG bevorzugt.
    Ohne diesen Test misst man bloss, dass Kurse sich bewegen: „80 % gefuellt"
    klingt stark und ist nichtssagend, wenn 80 % auch in die Gegenrichtung
    laufen. Exakt die Lehre aus `backtest_fibonacci.py`, wo eine ZUFALLSLINIE
    gleicher Distanz genauso gut hielt (55,7 % gegen 54,3 %).
  · KONTROLLE (B) — Nicht-Luecken-Tage, gleiche Distanz, gleiche Richtung.
  · 2 Stichproben (chronologische Haelften) + 95-%-Bootstrap je Zelle.

⚠⚠ ENTSCHEIDUNGSREGEL, VORAB FIXIERT: ein Magnet gilt nur als belegt, wenn die
Fuellquote die Kontrolle (A) in BEIDEN Haelften schlaegt UND das Bootstrap-
Intervall der Differenz die Null ausschliesst. Ein hoher Absolutwert allein ist
KEIN Beleg.

Aufruf:  python backtest_gaps_closeopen.py [min_gap] [tage]     (Default 0.5 / 10)
"""
from __future__ import annotations

import random
import sys

import MetaTrader5 as mt5

from core.stichprobe import anteil_ci

_MIN_CO = 0.50          # wie `[gaps] min_close_open` live
_HORIZONTE = (1, 2, 3, 5, 10, 20)


def hole_d1(n=5000):
    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_D1, 0, n)
    mt5.shutdown()
    if bars is None:
        raise SystemExit("keine D1-Bars")
    return sym, [{"o": float(b["open"]), "h": float(b["high"]),
                  "l": float(b["low"]), "c": float(b["close"]),
                  "t": int(b["time"])} for b in bars]


def finde_luecken(rows, min_co):
    """Exakt die Bedingung aus `core/gaps.py`: KEIN Vakuum, |Schluss→Open| >= min."""
    out = []
    for i in range(1, len(rows)):
        p, cur = rows[i - 1], rows[i]
        if cur["l"] > p["h"] or cur["h"] < p["l"]:
            continue                       # das ist ein Vakuum — andere Klasse
        co = cur["o"] - p["c"]
        if abs(co) < min_co:
            continue
        out.append({"i": i, "dir": 1 if co > 0 else -1,
                    "size": abs(co), "fill": p["c"], "open": cur["o"],
                    "t": cur["t"]})
    return out


def erreicht(rows, i, ziel, richtung, tage):
    """Beruehrt der Kurs `ziel` binnen `tage` Bars? Der Luecken-Bar zaehlt MIT
    (eine Eroeffnungsluecke wird oft am selben Tag zugehandelt — so macht es auch
    `core/gaps.py`, waehrend ein Vakuum sich per Konstruktion nicht selbst
    fuellen kann)."""
    for r in rows[i:i + tage]:
        if richtung < 0 and r["l"] <= ziel:
            return True
        if richtung > 0 and r["h"] >= ziel:
            return True
    return False


def zeile(lbl, treffer, n):
    if n == 0:
        return f"  {lbl:<26}      —"
    a, lo, hi = anteil_ci(treffer, n)
    ci = f"[{100*lo:.0f} … {100*hi:.0f} %]" if lo is not None else "—"
    return f"  {lbl:<26}{n:>5}{100*a:>8.1f} %{ci:>20}"


def main():
    min_co = float(sys.argv[1]) if len(sys.argv) > 1 else _MIN_CO
    tage = int(sys.argv[2]) if len(sys.argv) > 2 else 10
    sym, rows = hole_d1()
    gl = finde_luecken(rows, min_co)
    mid = len(gl) // 2

    print("=" * 96)
    print(f"  EROEFFNUNGSLUECKEN (`close_open`) — {sym}, {len(rows)} D1-Bars, "
          f"{len(gl)} Luecken >= {min_co} $")
    print("  Frage: fuellt sie sich HAEUFIGER, als der Kurs dieselbe Strecke")
    print("         ohnehin in die GEGENrichtung laeuft?")
    print("=" * 96)

    for name, teil in (("H1 (aeltere Haelfte)", gl[:mid]), ("H2 (juengere)", gl[mid:])):
        print(f"\n  ── {name} — {len(teil)} Luecken ──")
        print(f"  {'':<26}{'n':>5}{'Quote':>10}{'95-%-KI':>20}")
        for h in _HORIZONTE:
            fu = [erreicht(rows, g["i"], g["fill"], -g["dir"], h) for g in teil]
            # KONTROLLE A: gleiche Distanz, GEGENrichtung, dieselbe Bar
            ko = [erreicht(rows, g["i"], g["open"] + g["dir"] * g["size"],
                           g["dir"], h) for g in teil]
            nf, nk = sum(fu), sum(ko)
            a1, l1, h1 = anteil_ci(fu, len(fu))
            a2, _, _ = anteil_ci(ko, len(ko))
            # Bootstrap der DIFFERENZ (gepaart — beide auf denselben Luecken)
            r = random.Random(42); m = len(teil)
            d = sorted((sum(fu[r.randrange(m)] for _ in range(m))
                        - sum(ko[r.randrange(m)] for _ in range(m))) / m
                       for _ in range(3000)) if m >= 10 else []
            dci = f"[{100*d[75]:+.0f} … {100*d[2924]:+.0f} Pp]" if d else "—"
            flag = "✅" if d and d[75] > 0 else ("❌" if d and d[2924] < 0 else "⚠")
            print(f"  {f'{h:>2} Tage  Fuellung':<26}{len(fu):>5}{100*a1:>9.1f} %"
                  f"{f'[{100*l1:.0f} … {100*h1:.0f} %]':>20}"
                  f"   Kontrolle {100*a2:>5.1f} %   Diff {dci:>20} {flag}")

    # ── KONTROLLE B: Nicht-Luecken-Tage, gleiche Distanz, gleiche Richtung ──
    print(f"\n  ── Kontrolle B: Nicht-Luecken-Tage ({tage} Tage) ──")
    idx_gap = {g["i"] for g in gl}
    r = random.Random(7)
    proben = [i for i in range(1, len(rows) - tage) if i not in idx_gap]
    r.shuffle(proben)
    for lbl, richt in (("Ruecklauf nach UNTEN", -1), ("Lauf nach OBEN", 1)):
        tr = []
        for i in proben[:600]:
            g = r.choice(gl)["size"]           # gleiche Distanzverteilung
            tr.append(erreicht(rows, i, rows[i]["o"] + richt * g, richt, tage))
        print(zeile(lbl, tr, len(tr)))

    print("\n  " + "-" * 92)
    print("  ⚠ REGEL (vorab): Magnet nur belegt, wenn die Fuellquote die Kontrolle A")
    print("    in BEIDEN Haelften schlaegt UND das Differenz-KI die Null ausschliesst.")
    print("  ⚠ Ein hoher Absolutwert allein ist KEIN Beleg — Kurse bewegen sich.")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
