diff --git a/CLAUDE.md b/CLAUDE.md index 933b9b8..5b24f16 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2162,6 +2162,32 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: `_SR_BONUS`/`_SR_PENALTY`), Close-Alarme und Verdict-Kontext — und so indirekt in den **Autotrader** (der auf die Welle hört). **Keine** eigene Handelsrichtung. Snapshot `gaps`. Mess-/Einzeltool: `find_gaps.py`. + - **⚠⚠ ERKENNUNG WAR DEFEKT — zwei Fehler (Fix 2026-08-03, User-Meldung „die + Kurslückenerkennung funktioniert nicht richtig"):** + **(1) Die größte Lücke des Jahres wurde um 3 Cent verfehlt.** Die Definition + verlangte ein lückenloses **Vakuum zwischen den TAGESSPANNEN**. Am Wochenende + 31.07.→03.08. fiel WTI von Schluss **86,33** auf Eröffnung **79,76** = **−6,57 $** — + unsichtbar, weil Montags Hoch (81,67) das Freitags-Tief (81,64) um **0,03 $** + überlappte. In einem 24/5-Markt überlappen die Tagesspannen fast immer; die + strenge Regel fand im ganzen Jahr nur **5** Gaps. + **(2) Alle Gap-Datumsangaben waren einen Tag zu früh.** `int(b["time"]) − + _BROKER_OFFSET_S` zog 3 h von einer Bar-Zeit ab, die MT5 bereits als naive + Broker-Zeit liefert → jeder D1-Bar rutschte auf den Vortag (die gemeldete Lücke + „2026-07-26" war real der **27.07.**). Am Jahreswechsel hätte es zusätzlich den + `year`-Filter verschoben. + **Fix:** zwei getrennte Gap-Begriffe im Snapshot (`kind`): + · **`vacuum`** = die alte, strenge Definition — **NUR diese speisen `zone_lines()` + → `_sr_levels` → Wellen-Konfidenz**, denn nur auf ihnen wurde gemessen. Der + Strategie-Eingang ist damit **bitgenau unverändert** (verifiziert: `zone_lines()` + liefert vorher wie nachher `[88.288]`). + · **`close_open`** = Schluss→Eröffnung, also das, was ein Mensch „Kurslücke" nennt. + **REINE ANZEIGE**, ab `min_close_open`=0,50 $, füllt auch am selben Tag. + Ergebnis live: 4 offene Lücken statt 1 (1 Vakuum + 3 Eröffnungslücken), darunter + endlich die **−6,57-$-Wochenendlücke mit Fill bei 86,33**. Frontend kennzeichnet + beide Arten (`Vakuum` / `Eröffnung`) mit Tooltip, v=133. + ⚠ **Bewusst NICHT getan:** die `close_open`-Lücken in die Konfidenz einzuspeisen. + Das wäre ein ungemessener Strategie-Eingriff — und der gemessene Befund unten + beruht auf der Vakuum-Definition. Wer sie einbeziehen will, misst vorher. - **Gap-Magnet als Handelssignal = VERWORFEN (gemessen, `backtest_gaps.py`):** Signale WEG vom nächsten offenen Gap haben **+0,094** Ø-Edge, ZUM Gap nur **+0,014** — der Magnet zieht NICHT (das Gap blieb offen, weil der Trend von @@ -2247,7 +2273,7 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: Signal → **Hinweis-Dialog „trotzdem eröffnen?"** (überschreibbar). CLOSE bei **negativem** P&L → Sicherheitsabfrage; im Plus/Breakeven direkt. Sonst keine Token-Dialoge (`require_token=false`). Audio braucht 1× Nutzer-Tap. -- Asset-Version aktuell **v=132** (in `web/index.html` hochzählen, siehe Workflows). +- Asset-Version aktuell **v=133** (in `web/index.html` hochzählen, siehe Workflows). Schriftgrößen 2026-07-24 global **+4px** (2× je +2px auf User-Wunsch; Body-Basis 14→18px). ⚠ Betrifft in `style.css` sowohl `font-size:Npx` (71×) ALS AUCH die `font: Npx/…`-**Shorthand** (3×: `.ms-chip`/`.ms-bos-lbl`/`.sqm-badge` — diff --git a/core/gaps.py b/core/gaps.py index 09d801d..c5773eb 100644 --- a/core/gaps.py +++ b/core/gaps.py @@ -6,10 +6,28 @@ Fill-Target-Levels als S/R-Kandidaten (Magnete). Gleiche Rolle wie `daily_levels / `SRDetector`: NUR Kontext/Anzeige + Konfidenz (über `_sr_levels` → Wellen- Konfidenz → Autotrader), KEINE eigene Handelsrichtung. -Gap-Definition (Vakuum zwischen Vortag und Folgetag): - UP : Low(heute) > High(gestern) → Lücke [High_gestern … Low_heute], füllt bei High_gestern - DOWN : High(heute) < Low(gestern) → Lücke [High_heute … Low_gestern], füllt bei Low_gestern -„Gefüllt" = ein späterer Bar handelt wieder in/durch das Vakuum. +ZWEI Gap-Begriffe — bewusst getrennt (Fix 2026-08-03): + + 1. `kind="vacuum"` — **Vakuum zwischen den TAGESSPANNEN** (die strenge Definition): + UP : Low(heute) > High(gestern) → Lücke [High_gestern … Low_heute] + DOWN : High(heute) < Low(gestern) → Lücke [High_heute … Low_gestern] + Nur DIESE speisen `zone_lines()` → `_sr_levels` → Wellen-Konfidenz, denn nur auf + ihnen wurde gemessen (`backtest_gaps.py`: „der Magnet zieht NICHT" — Signale WEG + vom Gap +0,094 Ø-Edge, ZUM Gap nur +0,014). Die Definition zu ändern würde diese + Messung ungültig machen, also bleibt der Strategie-Eingang unangetastet. + + 2. `kind="close_open"` — **Schlusskurs → Eröffnung**, also das, was ein Mensch als + „Kurslücke" bezeichnet. REINE ANZEIGE, fließt NICHT in `zone_lines()`. + +⚠ WARUM das nötig wurde (User-Meldung „die Kurslückenerkennung funktioniert nicht +richtig"): Am Wochenende 31.07.→03.08. fiel WTI von Schluss 86,33 auf Eröffnung +79,76 — **−6,57 $**. Die Vakuum-Definition sah davon NICHTS, weil Montags Hoch +(81,67) das Freitags-Tief (81,64) um **3 Cent** überlappte. Ein Detektor, der die +größte Lücke des Jahres an drei Cent scheitern lässt, beantwortet die Frage nicht, +die der Nutzer stellt. In einem 24/5-Markt überlappen die Tagesspannen fast immer — +deshalb fand die strenge Regel im ganzen Jahr nur 5 Gaps. + +„Gefüllt" = ein späterer Bar handelt wieder in/durch die Lücke. """ from __future__ import annotations import threading @@ -30,7 +48,12 @@ class GapAnalyzer: """Thread-sicher, fail-safe. `maybe_refresh()` drosselt den MT5-Zugriff.""" def __init__(self, refresh_s: int = 600, lookback_days: int = 400, - year: int | None = None): + year: int | None = None, min_close_open: float = 0.50): + # `min_close_open`: ab welcher Schluss→Eröffnung-Differenz eine Lücke + # gemeldet wird. 0,50 $ filtert das normale Wochenend-Rauschen heraus und + # lässt die handelsrelevanten Sprünge übrig (real gefunden: +1,17 / +1,13 / + # +1,05 / −6,57 $ in den letzten Wochen). + self._min_co = float(min_close_open) self._lock = threading.Lock() self._gaps: list[dict] = [] # offene (ungefüllte) Gaps self._n_total = 0 @@ -59,9 +82,15 @@ class GapAnalyzer: year = self._year or dt.datetime.now().year rows = [] for b in bars: + # ⚠ DATUMS-FIX 2026-08-03: hier stand `int(b["time"]) - _BROKER_OFFSET_S`. + # MT5 liefert die Bar-Zeit als NAIVE Broker-Zeit, die bereits als Epoch + # kodiert ist — ein zusätzlicher Abzug von 3 h schiebt jeden D1-Bar auf + # den VORTAG. Real: der Bar vom 03.08. wurde als „2026-08-02" geführt, + # die gemeldete Lücke „2026-07-26" war in Wahrheit der 27.07. Betraf + # ALLE Gap-Datumsangaben (und am Jahreswechsel den `year`-Filter). d = dt.datetime.fromtimestamp( - int(b["time"]) - _BROKER_OFFSET_S, tz=dt.timezone.utc).date() - rows.append({"date": d, "h": float(b["high"]), + int(b["time"]), tz=dt.timezone.utc).date() + rows.append({"date": d, "h": float(b["high"]), "o": float(b["open"]), "l": float(b["low"]), "c": float(b["close"])}) rows = [r for r in rows if r["date"].year == year] if len(rows) < 2: @@ -72,16 +101,35 @@ class GapAnalyzer: p, cur = rows[i - 1], rows[i] if cur["l"] > p["h"]: gaps.append({"i": i, "date": cur["date"].isoformat(), "dir": "up", + "kind": "vacuum", "lo": round(p["h"], 3), "hi": round(cur["l"], 3), "fill": round(p["h"], 3)}) elif cur["h"] < p["l"]: gaps.append({"i": i, "date": cur["date"].isoformat(), "dir": "down", + "kind": "vacuum", "lo": round(cur["h"], 3), "hi": round(p["l"], 3), "fill": round(p["l"], 3)}) + else: + # Schluss → Eröffnung: die Lücke, die ein Mensch meint. Nur wenn + # KEIN Vakuum vorliegt (sonst doppelt), und erst ab `min_close_open`. + co = cur["o"] - p["c"] + if abs(co) >= self._min_co: + lo, hi = sorted((p["c"], cur["o"])) + gaps.append({"i": i, "date": cur["date"].isoformat(), + "dir": "up" if co > 0 else "down", + "kind": "close_open", + "lo": round(lo, 3), "hi": round(hi, 3), + # Füllung = zurück zum Schlusskurs des Vortags + "fill": round(p["c"], 3)}) openg = [] for g in gaps: - later = rows[g["i"] + 1:] + # ⚠ Bei `close_open` zählt der Gap-Bar SELBST mit: eine Eröffnungs- + # lücke wird oft am selben Tag wieder zugehandelt („Gap am Montag + # geschlossen"). Ein Vakuum kann sich per Konstruktion nicht selbst + # füllen (es liegt ja zwischen den Spannen), dort erst ab dem Folgetag. + later = rows[g["i"]:] if g.get("kind") == "close_open" \ + else rows[g["i"] + 1:] if g["dir"] == "up": mn = min((r["l"] for r in later), default=g["hi"]) filled = mn <= g["lo"] @@ -102,9 +150,18 @@ class GapAnalyzer: log.warning(f"GapAnalyzer.detect: {e}") def zone_lines(self) -> list[float]: - """Fill-Target-Levels (für die S/R-Kandidaten / Magnete).""" + """Fill-Target-Levels für `_sr_levels` → Wellen-Konfidenz. + + ⚠ NUR `vacuum`-Gaps (Fix 2026-08-03). Der gemessene Befund + (`backtest_gaps.py`) beruht auf genau dieser Definition; die 2026-08-03 + ergänzten `close_open`-Lücken sind REINE ANZEIGE und würden den + Strategie-Eingang sonst still verändern — genau die Sorte unbemerkter + Grundlagen-Verschiebung, gegen die der Deployment-Drift-Abschnitt steht. + Wer sie einbeziehen will, misst vorher (2 Stichproben). + """ with self._lock: - return [g["fill"] for g in self._gaps] + return [g["fill"] for g in self._gaps + if g.get("kind", "vacuum") == "vacuum"] def nearest(self, price: float | None) -> dict: """Nächstes offenes Gap-Fill-Level über/unter dem Preis.""" @@ -119,5 +176,10 @@ class GapAnalyzer: def snapshot(self) -> dict: with self._lock: - return {"gaps": list(self._gaps), "n_open": len(self._gaps), + g = list(self._gaps) + return {"gaps": g, "n_open": len(g), + # getrennt ausweisen, damit im UI erkennbar bleibt, welche + # Lücken die Konfidenz speisen (vacuum) und welche nur Anzeige sind + "n_vacuum": sum(1 for x in g if x.get("kind", "vacuum") == "vacuum"), + "n_close_open": sum(1 for x in g if x.get("kind") == "close_open"), "n_total": self._n_total, "last_update": self._ts} diff --git a/web/app.js b/web/app.js index 528e79d..0e002b1 100644 --- a/web/app.js +++ b/web/app.js @@ -211,9 +211,17 @@ function renderGaps(g, m) { const arr = up ? "▲" : "▼"; const dist = bid != null ? (x.fill - bid) : null; const ds = dist != null ? ` (${dist >= 0 ? "+" : ""}${fmt(dist)})` : ""; + // ⚠ Die zwei Gap-Arten unterscheidbar machen (2026-08-03): nur `vacuum` + // speist die Wellen-Konfidenz (darauf wurde gemessen), `close_open` ist + // reine Anzeige. Ohne den Hinweis sähen beide gleich verbindlich aus. + const co = x.kind === "close_open"; + const tag = co + ? `Eröffnung` + : `Vakuum`; return `
${arr} ${x.dir.toUpperCase()}` + `${fmt(x.lo)}–${fmt(x.hi)}` + `schließt bei ${fmt(x.fill)}${ds}` + + tag + `${x.date}
`; }).join(""); } diff --git a/web/index.html b/web/index.html index 4d3e1eb..11f172d 100644 --- a/web/index.html +++ b/web/index.html @@ -6,7 +6,7 @@ Oil · MT5 - + @@ -249,6 +249,6 @@ - +