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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
47c596ed70
commit
06ebb014be
@@ -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())
|
||||
Reference in New Issue
Block a user