diff --git a/CLAUDE.md b/CLAUDE.md index 5a2e2c5..5cb9f47 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3932,6 +3932,47 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: **+0,014** — der Magnet zieht NICHT (das Gap blieb offen, weil der Trend von ihm weglief). „Richtung Gap handeln" wäre schädlich; „weg" ist nur Trendbestätigung (macht die Welle schon). Gaps bleiben reiner Kontext. + - **⚠⚠ ERÖFFNUNGSLÜCKEN (`close_open`) NACHGEMESSEN = AUCH KEIN MAGNET + (`backtest_gaps_closeopen.py`, 2026-08-11, User: „vielleicht will der Kurs + erst die Kurslücke schliessen?").** Die Frage war **berechtigt offen**: der + Befund darüber beruht auf der **`vacuum`**-Definition, und `core/gaps.py` + sagt ausdrücklich, dass nur die gemessen wurde („wer `close_open` einbeziehen + will, misst vorher"). Die Klasse, die ein Mensch „Kurslücke" nennt + (Schluss → Eröffnung, typisch die Wochenendlücke), war **nie geprüft**. + 124 Lücken ≥0,50 $ über 4.359 D1-Bars, 2 Hälften, 95-%-Bootstrap. + ⚠⚠ **Die Füllquote sieht überzeugend aus — und ist nichtssagend:** + | Horizont | H1 Füllung / **Kontrolle** | H2 Füllung / **Kontrolle** | + |---|---|---| + | 3 Tage | 71,0 % / **69,4 %** | 74,2 % / **74,2 %** | + | 5 Tage | 80,6 % / **77,4 %** | 75,8 % / **75,8 %** | + | 10 Tage | 88,7 % / **83,9 %** | 80,6 % / **80,6 %** | + | 20 Tage | 90,3 % / **87,1 %** | 83,9 % / **87,1 %** ⚠ Kontrolle höher | + ✅ **Die Kontrolle ist der ganze Befund: dieselbe Distanz, dieselbe Bar, aber + GEGENrichtung.** Damit sind Volatilität, Regime und Zeitraum automatisch + gleich — übrig bleibt nur die Frage, ob die Lücke eine RICHTUNG bevorzugt. + In H2 ist sie bei 3/5/10 Tagen **exakt identisch**. **Alle 24 Differenz-KI + enthalten die Null**, die vorab fixierte Regel ist in keiner Zelle erfüllt. + Kontrolle B bestätigt: an Nicht-Lücken-Tagen läuft der Kurs dieselbe Strecke + binnen 10 Tagen zu **73,5 % nach unten und 75,5 % nach oben** — die ~75–80 % + sind schlicht die Basisrate eines 1–2-$-Laufs in WTI, keine Eigenschaft der + Lücke. ⚠ **GROSSE Lücken ≥1,0 $ kippen zusätzlich das Vorzeichen** zwischen + den Hälften (H1 10/20 Tage: Kontrolle **schlägt** die Füllung 81,8/72,7 bzw. + 86,4/72,7; H2 umgekehrt) — bei n=22/23 das klassische Rauschmuster. + ⚠ **Fairerweise der einzige konsistente Punkt:** die **Ein-Tages**-Füllung + („Lücke wird am selben Tag zugehandelt") ist in beiden Hälften UND beiden + Läufen richtungs-positiv (+4,9/+9,7 bzw. +4,6/+21,7 Pp). Zwei gleichgerichtete + Hälften sind mehr als eine — „KI enthält 0" heisst **nicht widerlegt, sondern + nicht belegt**. Für einen Eingriff reicht es bei n=22–62 nicht. + ✅ **Dritte Bestätigung derselben Lehre**, nach Vakuum-Gaps und Fibonacci: + die Frage ist nie „füllt/hält es?", sondern **„häufiger als eine willkürliche + Linie gleicher Distanz?"**. Ohne diese Kontrolle misst man nur, dass Kurse + sich bewegen. **Keine Änderung** — `close_open` bleibt reine Anzeige und + fliesst weiterhin NICHT in `zone_lines()`. + ⚠ **Der Anlassfall entschied sich noch während der Messung dagegen:** die + Lücke vom 10.08. (Füllung 77,383) war am 11.08. nicht geschlossen — der Kurs + stand bei **82,849**, also **5,47 $ in der Gegenrichtung**. Ein Einzelfall + belegt nichts, aber er illustriert den Mechanismus des Ursprungsbefunds: das + Gap blieb offen, **weil** der Trend von ihm weglief. - **Tagesziele — Dashboard-Kachel ENTFERNT (2026-07-13):** Das Frontend zeigt die Web-Prognose nicht mehr. Das Modul `daily_levels` läuft **unverändert weiter**: die Level fließen als **S/R-Kandidaten** in `_sr_levels` (Anzeige/Close/Verdict-Kontext) diff --git a/backtest_gaps_closeopen.py b/backtest_gaps_closeopen.py new file mode 100644 index 0000000..943bbe5 --- /dev/null +++ b/backtest_gaps_closeopen.py @@ -0,0 +1,161 @@ +#!/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())