Kurslueckenerkennung: zwei Fehler behoben

User-Meldung "die Kurslueckenerkennung funktioniert nicht richtig". Zwei Fehler
gefunden:

1) Die groesste Luecke des Jahres wurde um 3 Cent verfehlt. Die Definition
   verlangte ein lueckenloses Vakuum zwischen den TAGESSPANNEN. Am Wochenende
   31.07.->03.08. fiel WTI von Schluss 86,33 auf Eroeffnung 79,76 = -6,57 $ -
   unsichtbar, weil Montags Hoch (81,67) das Freitags-Tief (81,64) um 0,03 $
   ueberlappte. In einem 24/5-Markt ueberlappen die Tagesspannen fast immer;
   die strenge Regel fand im ganzen Jahr nur 5 Gaps.

2) Alle Gap-Datumsangaben waren einen Tag zu frueh: int(b["time"]) minus
   _BROKER_OFFSET_S zog 3 h von einer Bar-Zeit ab, die MT5 bereits als naive
   Broker-Zeit liefert. Die gemeldete Luecke "2026-07-26" war real der 27.07.
   Am Jahreswechsel haette es zusaetzlich den year-Filter verschoben.

Fix: zwei getrennte Gap-Begriffe (kind).
- vacuum     = alte strenge Definition. NUR diese speisen zone_lines() ->
               _sr_levels -> Wellen-Konfidenz, denn nur auf ihnen wurde
               gemessen (backtest_gaps.py). Strategie-Eingang bitgenau
               unveraendert: zone_lines() liefert vorher wie nachher [88.288].
- close_open = Schluss->Eroeffnung, das was ein Mensch "Kurslucke" nennt.
               REINE ANZEIGE, ab 0,50 $, fuellt auch am selben Tag.

Live: 4 offene Luecken statt 1 (1 Vakuum + 3 Eroeffnungsluecken), darunter
endlich die -6,57-$-Wochenendluecke mit Fill bei 86,33. Frontend kennzeichnet
beide Arten mit Tooltip. v=133.

Bewusst NICHT getan: close_open in die Konfidenz einspeisen - das waere ein
ungemessener Strategie-Eingriff, und der vorhandene Befund beruht auf der
Vakuum-Definition.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-03 18:19:48 +02:00
co-authored by Claude Opus 5
parent 5f94917014
commit fe7a1acb97
4 changed files with 110 additions and 14 deletions
+27 -1
View File
@@ -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:<weight> Npx/…`-**Shorthand** (3×: `.ms-chip`/`.ms-bos-lbl`/`.sqm-badge`
+73 -11
View File
@@ -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}
+8
View File
@@ -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
? `<span class="dim" title="Schluss→Eröffnung — reine Anzeige, fließt NICHT in die Konfidenz">Eröffnung</span>`
: `<span class="dim" title="Vakuum zwischen den Tagesspannen — speist die S/R-Magnete">Vakuum</span>`;
return `<div class="gap-row"><span class="${up ? "up" : "down"}">${arr} ${x.dir.toUpperCase()}</span>`
+ `<span class="gap-zone">${fmt(x.lo)}${fmt(x.hi)}</span>`
+ `<span class="gap-fill">schließt bei <b>${fmt(x.fill)}</b>${ds}</span>`
+ tag
+ `<span class="dim gap-date">${x.date}</span></div>`;
}).join("");
}
+2 -2
View File
@@ -6,7 +6,7 @@
<meta name="theme-color" content="#0d1117">
<title>Oil · MT5</title>
<link rel="manifest" href="manifest.json">
<link rel="stylesheet" href="style.css?v=132">
<link rel="stylesheet" href="style.css?v=133">
</head>
<body>
<!-- Modul-Menü (ganz oben) -->
@@ -249,6 +249,6 @@
<div id="toast" class="toast hidden"></div>
<script src="lightweight-charts.standalone.production.js?v=114"></script>
<script src="app.js?v=132"></script>
<script src="app.js?v=133"></script>
</body>
</html>