Files
AH-Oil-Trader/backtest_gaps_closeopen.py
Axel HocksandClaude Opus 5 06ebb014be Eroeffnungsluecken (close_open) gemessen: auch kein Magnet
User-Frage "vielleicht will der Kurs erst die Kursluecke schliessen?"
war berechtigt offen: der vorhandene Gap-Befund (backtest_gaps.py)
beruht auf der vacuum-Definition, und core/gaps.py sagt ausdruecklich,
dass nur die gemessen wurde. Die Klasse, die ein Mensch "Kursluecke"
nennt (Schluss -> Eroeffnung, Wochenendluecke), war nie geprueft.

124 Luecken >= 0,50 $ ueber 4.359 D1-Bars, 2 Haelften, Bootstrap-KI.
Fuellquote sieht stark aus (75-89 % in 10 Tagen) - die Kontrolle
(gleiche Distanz, gleiche Bar, GEGENrichtung) liegt praktisch gleich
auf, in H2 bei 3/5/10 Tagen exakt identisch. Alle 24 Differenz-KI
enthalten die Null. Kontrolle B: an Nicht-Luecken-Tagen laeuft der
Kurs dieselbe Strecke in 10 Tagen zu 73,5 % nach unten und 75,5 %
nach oben - die ~75-80 % sind die Basisrate, keine Gap-Eigenschaft.
Grosse Luecken >= 1,0 $ kippen zusaetzlich das Vorzeichen zwischen
den Haelften (n=22/23).

Dritte Bestaetigung derselben Lehre nach Vakuum-Gaps und Fibonacci:
die Frage ist nie "faellt/haelt es?", sondern "haeufiger als eine
willkuerliche Linie gleicher Distanz?".

Keine Aenderung - close_open bleibt reine Anzeige, fliesst weiter
NICHT in zone_lines().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 08:27:46 +02:00

162 lines
7.3 KiB
Python

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