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