From 516f1eaa7fd7d4069be875d6fc7e201844c018e8 Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Wed, 5 Aug 2026 12:45:36 +0200 Subject: [PATCH] Roast-Punkte 1-3: Erfolgskontrolle erweitert, flip_close sichtbar, WARTEN-Massstab korrigiert - und ein eigener Messfehler gefunden MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (3) backtest_dist_gated.py: der 43-%-Massstab war UNGUELTIG. backtest_dist.py misst _build ALLEIN, ohne Breakout-Bestaetigung und ohne Entry-Raum-Gate - genau die beiden sind live die groessten Blocker. Ueber 80k Bars: nur _build 77,9 % gerichtet, voller Live-Stack 19,5 %, live 7,2 %. Der reale Abstand ist 12,3 Pp, nicht 50. Damit ist der "93 statt 43 %"-Alarm entschaerft, der den TF-Churn-Fix ausgeloest hat; die dortige Erfolgskontrolle ist hinfaellig. Ein Rest bleibt (min_conf live 16,3 % gegen 0,9 % in der Sim) und ist als naechster Ansatzpunkt notiert. ⚠ Dabei EIGENEN Fehler gefunden: set_entry_room(0.6) fehlte in DREI heute neu gebauten Skripten - _entry_room_atr ist im Konstruktor 0.0, _room_gate ist dann ein No-op. Aufgefallen, weil entry_room in der Verteilung mit 0 % auftauchte statt mit 35 %. Alle drei korrigiert und die SIG-Messung WIEDERHOLT: Population aendert sich stark (n 967->295 / 1327->524), die Schlussfolgerung nicht - Market in 7 von 8 Feldern negativ, am Level bestehen alle vier Schwellen (OeR +0,392/+0,356, PF 2,37/2,39), Slippage-Test haelt bis 0,20 xATR. Die heutige Bau-Entscheidung ist gedeckt. (2) auto_flip_close ist jetzt sichtbar (#flip-note, v=139) - mit Schwelle, Zaehler UND der Messung im Text ("gemessen in beiden Halbjahren negativ"). Ein blanker Zaehler haette wie ein Erfolg ausgesehen. Der Exit bleibt an. (1) analyze_squeeze_entry_gap.py erfasst jetzt BEIDE Bot-Pfade (Squeeze + Signal), Reminder entsprechend erweitert - sonst wartet er auf eine Population, die vielleicht nie kommt. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 51 ++++++++- analyze_squeeze_entry_gap.py | 44 ++++++-- backtest_auto_signal_v3.py | 10 +- backtest_dist_gated.py | 198 +++++++++++++++++++++++++++++++++++ backtest_metalabel.py | 10 +- measurement_reminder.py | 8 +- web/app.js | 19 ++++ web/index.html | 10 +- 8 files changed, 326 insertions(+), 24 deletions(-) create mode 100644 backtest_dist_gated.py diff --git a/CLAUDE.md b/CLAUDE.md index 9270c5e..6de6edf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -331,6 +331,29 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: (`backtest_conf.py`): Band **40–54 % = negativer Edge** (−0,025), ≥55 % = +0,058; Gate hebt Ø-Edge ~+50 % und den Gesamtertrag (entfernt die schwachen Setups, bei denen zu viele Strafen gestapelt sind — typisch der Lagging-Short am Boden). +- **⚠⚠ DER 43-%-MASSSTAB WAR UNGÜLTIG — der richtige liegt bei ~80 % WARTEN + (gemessen `backtest_dist_gated.py`, 2026-08-05).** `backtest_dist.py` misst + **`_build` ALLEIN**, ohne **Breakout-Bestätigung** und ohne **Entry-Raum-Gate**. + Genau die beiden sind live die größten Blocker. Über 80k M5-Bars: + | | gerichtet | WARTEN | + |---|---|---| + | nur `_build` (die alte Referenz) | **77,9 %** | 22,1 % | + | **voller Live-Stack** | **19,5 %** | **80,5 %** | + | LIVE (seit 01.08., ohne `stale`) | 7,2 % | 92,8 % | + Aufschlüsselung im vollen Stack: entry_room **41,4 %** · breakout_pending 17,0 % · + stretch 13,8 % · deadband 7,4 % · min_conf 0,9 %. + ✅ **Damit ist der „93 % statt 43 %"-Alarm entschärft**, der den TF-Churn-Fix + (2026-07-31) ausgelöst hat: verglichen wurden zwei verschiedene Systeme. Der reale + Abstand zum gültigen Maßstab ist **12,3 Pp**, nicht 50. + ⚠ **Ein Rest bleibt** (19,5 → 7,2 %) und ist NICHT wegerklärt. Kandidaten, die eine + Bar-Simulation prinzipiell nicht abbildet: der **TF-Wechsel** (live wandert die + Basis-TF M5…M30, die Sim ist fix M5), Cooldowns, EIA-/News-Fenster, und die + **Abtastung** — live wird pro Sekunde gemessen, ein `breakout_pending`-Zustand + hält über eine ganze Bar an und ist deshalb live überrepräsentiert (26,5 % gegen + 17,0 %). Der auffälligste Einzelposten ist **min_conf: live 16,3 %, Sim 0,9 %** — + das ist der nächste konkrete Ansatzpunkt, wenn jemand dem Rest nachgehen will. + ⚠ **Die im TF-Churn-Fix dokumentierte Erfolgskontrolle („WARTEN-Anteil Richtung + 43 % bewegen") ist damit hinfällig** und sollte nicht weiterverfolgt werden. - **Signalhäufigkeit** (`backtest_dist.py`): ~57 % der Zeit LONG/SHORT, ~43 % WARTEN. WARTEN-Treiber: M30-Gegen-Trend (~27 % der Bars, gewollt, Edge×2) + Überdehnung + Totband. `_STRETCH_MAX` 2,5→**3,5** angehoben (`backtest_stretch.py`: +26 % @@ -1436,6 +1459,21 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: ⚠ **B4-Pflicht:** dieselbe Erfolgskontrolle wie beim Squeeze — der Abstand Einstieg↔Level muss gegen 0 gehen. AUTOSIG-Trades sind über `setup=AUTOSIG_*` separierbar. + ⚠⚠ **ZWEITER Methodik-Fehler, am selben Tag gefunden und die Messung WIEDERHOLT: + `set_entry_room(0.6)` fehlte.** `WaveRecommender.__init__` setzt + `_entry_room_atr = 0.0`, und `_room_gate` gibt dann `rec` **unverändert** zurück — + der Aufruf sieht im Code aus wie ein Gate, ist aber ein No-op. Betroffen waren + DREI am 05.08. neu gebaute Skripte (`backtest_auto_signal_v3`, `_metalabel`, + `_dist_gated`); die Referenz `backtest_breakout_gated.py` (04.08.) macht es richtig. + Aufgefallen ist es erst, weil `entry_room` in der Verteilung mit **0 %** auftauchte, + live aber 35 % blockt. **Neu gerechnet ändert sich die Population stark (n 967→295 + bzw. 1327→524), die Schlussfolgerung aber NICHT:** Market bleibt in 7 von 8 Feldern + negativ, am Level bestehen weiterhin **alle vier** Konfidenz-Schwellen + (ØR +0,392/+0,356 · PF 2,37/2,39), und der Slippage-Härtetest hält bis 0,20×ATR + (+0,177/+0,118). **Die heutige Bau-Entscheidung ist damit gedeckt.** + ⚠ Lehre: ein aufgerufenes Gate ist nicht dasselbe wie ein aktives Gate. Beim + Kopieren einer Backtest-Struktur gehört die **Konfiguration** mitkopiert — sonst + misst man einen Stack, den es nicht gibt. ⚠ **Methodik-Fehler beim Bau, gefunden und behoben:** der erste Entwurf liess `pend["level"]` nach der Bestätigung über weitere Bars stehen — bei verzögertem Einstieg (Slot belegt, Konfidenz zu niedrig) wäre er zu einem Preis eingestiegen, @@ -2933,7 +2971,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=138** (in `web/index.html` hochzählen, siehe Workflows). +- Asset-Version aktuell **v=139** (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` — @@ -3649,10 +3687,13 @@ Funktionsreferenzen in CLAUDE.md stimmen · Asset- und Indikator-Version konsist ein reines Migrations-Artefakt (Spalte kam am 31.07.); seit 01.08. **100 % Abdeckung**. **Offen (nicht behoben, bewusst):** -- **`auto_flip_close=true` läuft ohne jede UI-Anzeige.** Ein Exit, der gemessen in - BEIDEN Hälften Geld kostet (−92/−160 R), schließt echte Trades; `flip_close`, - `flip_close_min_r` und `flip_close_count` haben keinen Frontend-Konsumenten. Nur - Log + Telegram. Entweder anzeigen oder abschalten — Entscheidung des Users. +- ✅ **ERLEDIGT 2026-08-05: `auto_flip_close` ist jetzt SICHTBAR** (`#flip-note` in + der Meldungen-Karte, v=139). Zeigt Schwelle und Auslöse-Zähler und **nennt + ausdrücklich die Messung** („⚠ gemessen in beiden Halbjahren negativ", −92/−160 R) + — ein blanker Zähler hätte wie ein Erfolg ausgesehen. Der Exit bleibt AN + (User-Wunsch), aber die Entscheidung ist jetzt informiert. Live-Stand: 4 + Auslösungen, +284,22 € (n=4 = ohne Aussagekraft). Abschalten: + `[trading] auto_flip_close=false`. - **`breakout_k=0,3` ruht weiter auf dem `LEGACY_BE10`-Exit** (Breakeven 1,0 statt live 1,3). Der Legacy-Recheck hat EIA und Dead-Hours nachgerechnet, `breakout_k` nicht. Kandidat für den nächsten Recheck. diff --git a/analyze_squeeze_entry_gap.py b/analyze_squeeze_entry_gap.py index 31b2aa5..a321c42 100644 --- a/analyze_squeeze_entry_gap.py +++ b/analyze_squeeze_entry_gap.py @@ -33,8 +33,17 @@ _BERLIN = ZoneInfo("Europe/Berlin") _BROKER = timezone(timedelta(hours=3)) _UTC = timezone.utc _UMBAU = datetime(2026, 8, 5, 8, 55, tzinfo=_BERLIN) # Pending-Entry ging live -_RX = re.compile(r"^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d).*AUTO-SQUEEZE-ENTRY: (\w+) " - r"@ Ausbruch ([\d.]+)") +# ⚠ BEIDE Market-Pfade erfassen (2026-08-05): seit dem Pending-Umbau laeuft auch +# der Signal-Entry ueber eine ruhende Stop-Order. Nur der FALLBACK schreibt noch +# eine Log-Zeile mit Level — die faengt dieses Muster ebenfalls ein. +_RX = re.compile(r"^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d).*" + r"(?:AUTO-SQUEEZE-ENTRY: (\w+) @ Ausbruch ([\d.]+)" + r"|AUTO-SIGNAL-ENTRY: (\w+) )") +# Der Signal-Log nennt kein Level (es steht nur im Snapshot) — dort wird der +# Abstand deshalb nicht berechnet, der Trade zaehlt aber als Market-Fallback. +_RX_SIG = re.compile(r"^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d).*AUTO-SIGNAL-ENTRY: (\w+) ") +_RX_SQ = re.compile(r"^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d).*AUTO-SQUEEZE-ENTRY: (\w+) " + r"@ Ausbruch ([\d.]+)") def _berlin(ts): @@ -57,20 +66,27 @@ def main(): seit = datetime.now(_BERLIN) - timedelta(days=tage) # ── 1) Log: Ausbruchs-Level je Market-Entry ────────────────────────── - log_eintraege = [] + log_eintraege, sig_fallback = [], [] for f in sorted(glob.glob(os.path.join(_HIER, "oil_widget.log*"))): try: with open(f, encoding="utf-8", errors="replace") as fh: for z in fh: - m = _RX.match(z) + m = _RX_SQ.match(z) if m: ts = datetime.strptime(m.group(1), "%Y-%m-%d %H:%M:%S") \ .replace(tzinfo=_BERLIN) if ts >= seit: log_eintraege.append((ts, m.group(2), float(m.group(3)))) + continue + m = _RX_SIG.match(z) + if m: + ts = datetime.strptime(m.group(1), "%Y-%m-%d %H:%M:%S") \ + .replace(tzinfo=_BERLIN) + if ts >= seit: + sig_fallback.append((ts, m.group(2))) except OSError: continue - log_eintraege.sort() + log_eintraege.sort(); sig_fallback.sort() # ── 2) ATR-Reihe für die Normierung ────────────────────────────────── mt5.initialize() @@ -127,17 +143,23 @@ def main(): print(" die Market-Order ist nur noch Fallback, und die Nachjagd-Bremse") print(" (squeeze_max_chase_atr) deckelt sie bei 0,20×ATR.") print() - print(" ⚠ Die eigentliche Erfolgsmeldung: Squeeze-Trades OHNE Log-Zeile —") - print(" das sind die per Pending-Order gefüllten.") - n_sq = 0 + print(" ⚠ Die eigentliche Erfolgsmeldung: Bot-Trades OHNE Market-Log-Zeile —") + print(" das sind die per Pending-Order gefüllten (Squeeze UND Signal).") + sig_ts = {t.timestamp() for t, _ in sig_fallback} + n_bot = 0 for t in trades: if t["entry_time"] < _UMBAU.timestamp(): continue - n_sq += 1 - if t["ticket"] not in getroffen: + n_bot += 1 + # Market-Fallback erkannt, wenn eine der beiden Log-Zeilen zeitlich passt + market = (t["ticket"] in getroffen + or any(abs(t["entry_time"] - s) <= 90 for s in sig_ts)) + if not market: ohne_log.append(t) - print(f" Trades seit dem Umbau: {n_sq} · davon ohne Market-Log-Zeile: " + print(f" Trades seit dem Umbau: {n_bot} · davon ohne Market-Log-Zeile: " f"{len(ohne_log)}") + print(f" (Market-Fallback-Zeilen im Log: Squeeze {len(log_eintraege)} · " + f"Signal {len(sig_fallback)})") for t in ohne_log[-10:]: print(f" {datetime.fromtimestamp(t['entry_time'], _BERLIN):%d.%m %H:%M} " f"{t['direction']:<5} @ {t['entry_price']:.3f} " diff --git a/backtest_auto_signal_v3.py b/backtest_auto_signal_v3.py index d53d128..1e6751f 100644 --- a/backtest_auto_signal_v3.py +++ b/backtest_auto_signal_v3.py @@ -234,8 +234,14 @@ def main(): if L[j] == min(L[j - _PIV_K:j + _PIV_K + 1])] w = WaveRecommender(lambda: None) - w.set_reversal_enabled(False) - w.set_dead_hours("") + w.set_reversal_enabled(False) # Live-Stand seit 04.08. + # ⚠⚠ OHNE DIESE ZEILE IST DAS ENTRY-RAUM-GATE AUS: `_entry_room_atr` + # ist im Konstruktor 0.0, und `_room_gate` gibt dann `rec` unveraendert + # zurueck — der Aufruf sieht aus wie ein Gate, ist aber ein No-op. + # Genau das ist mir am 05.08. in drei Skripten passiert (live blockt das + # Gate 35 % aller Empfehlungen, im Backtest 0 %). + w.set_entry_room(0.6) # Live-Wert + w.set_dead_hours("") # live leer mid = len(C) // 2 print("=" * 106) diff --git a/backtest_dist_gated.py b/backtest_dist_gated.py new file mode 100644 index 0000000..b5ccdc9 --- /dev/null +++ b/backtest_dist_gated.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""Signal-Verteilung mit dem VOLLEN Live-Gate-Stack (2026-08-05). + +WARUM ES DAS BRAUCHT — der 43-%-Maßstab ist ungültig: +`backtest_dist.py` liefert „~57 % gerichtet / ~43 % WARTEN". Diese Zahl kommt aber +aus **`_build` ALLEIN** — ohne **Entry-Raum-Gate** und ohne **Breakout-Bestätigung**. +Genau die beiden erzeugen live **62 % aller Blocks** (seit 01.08., echte Marktzeit: +entry_room 35,3 % · breakout_pending 26,5 %). Der Vergleich „live 93 % WARTEN gegen +erwartete 43 %" stellt also zwei verschiedene Systeme gegenüber — und das +dokumentierte Erfolgskriterium des TF-Churn-Fixes ist damit per Konstruktion +unerreichbar. + +Dieses Skript stellt den fehlenden Maßstab her: dieselbe Verteilung, aber über den +Stack, der live wirklich läuft (`_build` → `_confirm_breakout` → `_room_gate`). + +⚠ ABGRENZUNG: gezählt wird **je BAR**, live je Snapshot (~1×/s). Für den ANTEIL ist +das vergleichbar; für absolute Häufigkeiten nicht. +⚠ NICHT modelliert (live-only, ohne Bar-Entsprechung): EIA-Fenster, News-Blackout, +Cooldowns, Startup-Schonfrist, `stale`. Deren Anteil ist live klein bzw. wird beim +Vergleich herausgerechnet. + +Aufruf: python backtest_dist_gated.py [bars] +""" +import sys +from collections import Counter + +import MetaTrader5 as mt5 + +from core.analysis import calc_trend_angle +from core.wave_rec import (WaveRecommender, _EMA_FAST, _EMA_SLOW, _N_BARS, + _ANGLE_LR, _ANGLE_DEAD, _HTF_DEADBAND) + +_ATRMIN = 0.06 +_TIMEOUT_BARS = 12 +_PIV_K, _PIV_LOOK = 3, 300 +_ATR_FLOOR_PB = 0.12 +_K = 0.3 + +# Live-Referenz (seit 01.08., echte Marktzeit, `stale` herausgerechnet) — aus der +# `recommendations`-Tabelle; s. Roast-Analyse 2026-08-05. +_LIVE = {"(gerichtet)": 7.2, "entry_room": 35.3, "breakout_pending": 26.5, + "min_conf": 16.3, "htf_counter": 6.7, "stretch": 6.4, "deadband": 1.6} + + +def _ema_series(v, p): + k = 2 / (p + 1); out = [v[0]] + for x in v[1:]: + out.append(out[-1] + k * (x - out[-1])) + return out + + +def _atr_series(H, L, C, p=14): + tr = [0.0] + for i in range(1, len(C)): + tr.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1]))) + out = [None] + for i in range(1, len(C)): + seg = tr[max(1, i - p + 1):i + 1] + out.append(sum(seg) / len(seg)) + return out + + +def _htf_sign(C, i, n, atr): + if i < n * _EMA_SLOW: + return 0 + seg = C[i - n * _EMA_SLOW:i + 1:n] + if len(seg) < _EMA_SLOW + 2: + return 0 + ef = _ema_series(seg, _EMA_FAST)[-1]; es = _ema_series(seg, _EMA_SLOW)[-1] + dead = _HTF_DEADBAND * atr * (n ** 0.5) + return 1 if ef - es > dead else (-1 if es - ef > dead else 0) + + +def verteilung(w, H, L, C, A, EF, ES, AD, HTF, H1, PH, PL, lo, hi, mit_gates): + """→ Counter über die Block-Gründe. `mit_gates=False` = nur `_build` (die alte, + ungültige Referenz), True = voller Live-Stack.""" + z = Counter() + pend = None + for i in range(max(lo, _PIV_LOOK + _EMA_SLOW + 5), hi): + atr = A[i] + if not atr or atr < _ATRMIN: + continue + w._pb_feats = {"atr": max(atr, _ATR_FLOOR_PB)} + w._pb_levels = {"ph": [C[j] for j in PH if i - _PIV_LOOK <= j <= i - _PIV_K], + "pl": [C[j] for j in PL if i - _PIV_LOOK <= j <= i - _PIV_K]} + rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5, + htf_trend=HTF[i], h1_trend=H1[i], angle=AD[i]) + sig = rec.get("signal") + if not mit_gates: + z["(gerichtet)" if sig in ("LONG", "SHORT") + else (rec.get("block") or "(unmarkiert)")] += 1 + continue + # ── Breakout-Bestätigung (Mechanik 1:1 aus `_confirm_breakout`) ────── + if sig in ("LONG", "SHORT"): + d = 1 if sig == "LONG" else -1 + if pend is None or pend["dir"] != d: + pend = {"dir": d, "level": C[i] + d * _K * atr, + "invalid": C[i] - d * _K * atr, "confirmed": False, "bar": i} + if not pend["confirmed"]: + broke = (C[i] >= pend["level"]) if d > 0 else (C[i] <= pend["level"]) + against = (C[i] <= pend["invalid"]) if d > 0 else (C[i] >= pend["invalid"]) + if broke: + pend["confirmed"] = True + else: + if against or (i - pend["bar"]) > _TIMEOUT_BARS: + pend = {"dir": d, "level": C[i] + d * _K * atr, + "invalid": C[i] - d * _K * atr, + "confirmed": False, "bar": i} + z["breakout_pending"] += 1 + continue + else: + pend = None + z[rec.get("block") or "(unmarkiert)"] += 1 + continue + rec2 = w._room_gate(rec, C[i]) + if rec2.get("signal") not in ("LONG", "SHORT"): + z["entry_room"] += 1 + continue + z["(gerichtet)"] += 1 + return z + + +def zeig(titel, z): + ges = sum(z.values()) + print(f"\n {titel} (n={ges})") + print(f" {'Grund':<20}{'Anteil':>9}{'live':>9}{'Delta':>9}") + for g, n in z.most_common(): + p = 100.0 * n / max(1, ges) + lv = _LIVE.get(g) + print(f" {g:<20}{p:>8.1f} %" + + (f"{lv:>8.1f} %{p-lv:>+8.1f}" if lv is not None else f"{'—':>9}{'':>9}")) + return 100.0 * z["(gerichtet)"] / max(1, ges) + + +def main(): + n_bars = int(sys.argv[1]) if len(sys.argv) > 1 else 80000 + 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_M5, 0, n_bars) + mt5.shutdown() + H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars] + C = [float(b["close"]) for b in bars] + EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW) + A = _atr_series(H, L, C) + AD = [0.0] * len(C) + for i in range(_N_BARS, len(C)): + AD[i] = calc_trend_angle(C[i - _N_BARS:i + 1], _ANGLE_LR) - 90.0 + if abs(AD[i]) < _ANGLE_DEAD: + AD[i] = 0.0 + HTF = [_htf_sign(C, i, 6, A[i] or 0.1) for i in range(len(C))] + H1S = [_htf_sign(C, i, 12, A[i] or 0.1) for i in range(len(C))] + PH = [j for j in range(_PIV_K, len(C) - _PIV_K) + if H[j] == max(H[j - _PIV_K:j + _PIV_K + 1])] + PL = [j for j in range(_PIV_K, len(C) - _PIV_K) + if L[j] == min(L[j - _PIV_K:j + _PIV_K + 1])] + + w = WaveRecommender(lambda: None) + w.set_reversal_enabled(False) # Live-Stand seit 04.08. + # ⚠⚠ OHNE DIESE ZEILE IST DAS ENTRY-RAUM-GATE AUS: `_entry_room_atr` + # ist im Konstruktor 0.0, und `_room_gate` gibt dann `rec` unveraendert + # zurueck — der Aufruf sieht aus wie ein Gate, ist aber ein No-op. + # Genau das ist mir am 05.08. in drei Skripten passiert (live blockt das + # Gate 35 % aller Empfehlungen, im Backtest 0 %). + w.set_entry_room(0.6) # Live-Wert + w.set_dead_hours("") # live leer + + print("=" * 74) + print(f" SIGNAL-VERTEILUNG — {sym} M5, {len(C)} Bars") + print(" Der gueltige Massstab fuer den WARTEN-Anteil (s. Modulkopf)") + print("=" * 74) + g_ohne = zeig("A) NUR `_build` — die ALTE, ungueltige Referenz (~57 % erwartet)", + verteilung(w, H, L, C, A, EF, ES, AD, HTF, H1S, PH, PL, + 0, len(C), False)) + g_mit = zeig("B) VOLLER Live-Stack (+ Breakout-Bestaetigung + Entry-Raum)", + verteilung(w, H, L, C, A, EF, ES, AD, HTF, H1S, PH, PL, + 0, len(C), True)) + print("\n" + "=" * 74) + print(" ERGEBNIS") + print("=" * 74) + print(f" gerichtet nur `_build` {g_ohne:>6.1f} % (daher die 43-%-Erwartung)") + print(f" gerichtet voller Stack {g_mit:>6.1f} % <- der GUELTIGE Massstab") + print(f" gerichtet LIVE (seit 01.08.) {_LIVE['(gerichtet)']:>5.1f} %") + d = _LIVE["(gerichtet)"] - g_mit + print() + if abs(d) <= 3: + print(f" => Live liegt {d:+.1f} Pp am Massstab. Der WARTEN-Anteil ist NORMAL,") + print(" nicht defekt. Der 43-%-Alarm war ein Vergleichsfehler.") + elif d < 0: + print(f" => Live liegt {d:+.1f} Pp DARUNTER — es gibt also einen echten Rest,") + print(" den die Bar-Simulation nicht erklaert (Cooldowns, EIA, News, TF-Wechsel).") + else: + print(f" => Live liegt {d:+.1f} Pp DARUEBER — mehr Signale als der Stack hergibt.") + print() + + +if __name__ == "__main__": + main() diff --git a/backtest_metalabel.py b/backtest_metalabel.py index b956057..e7b0d33 100644 --- a/backtest_metalabel.py +++ b/backtest_metalabel.py @@ -260,8 +260,14 @@ def main(): vol_med = sorted(x for x in A if x)[len(A) // 2] w = WaveRecommender(lambda: None) - w.set_reversal_enabled(False) # live-Stand seit 2026-08-04 - w.set_dead_hours("") + w.set_reversal_enabled(False) # Live-Stand seit 04.08. + # ⚠⚠ OHNE DIESE ZEILE IST DAS ENTRY-RAUM-GATE AUS: `_entry_room_atr` + # ist im Konstruktor 0.0, und `_room_gate` gibt dann `rec` unveraendert + # zurueck — der Aufruf sieht aus wie ein Gate, ist aber ein No-op. + # Genau das ist mir am 05.08. in drei Skripten passiert (live blockt das + # Gate 35 % aller Empfehlungen, im Backtest 0 %). + w.set_entry_room(0.6) # Live-Wert + w.set_dead_hours("") # live leer mid = len(C) // 2 print("=" * 92) diff --git a/measurement_reminder.py b/measurement_reminder.py index ad15b02..d7a3fea 100644 --- a/measurement_reminder.py +++ b/measurement_reminder.py @@ -160,9 +160,13 @@ CHECKS = [ # Mitternacht: der Squeeze-Trade von 08:33 lief noch über die Market-Order und # würde die Zählung verfälschen. `strftime('%s', …)` liest UTC, `entry_time` # ist lokale Epoch — deshalb 07:00 UTC als sicherer Schnitt. - "have": lambda: _q("SELECT COUNT(*) FROM trades WHERE setup LIKE 'SQUEEZE%' " + # ⚠ Seit 05.08. laeuft AUCH der Signal-Pfad ueber Pending-Orders — beide + # zaehlen, sonst wartet der Reminder auf eine Population, die vielleicht + # nie kommt (der Squeeze feuert nur ~2-3x/Woche). + "have": lambda: _q("SELECT COUNT(*) FROM trades WHERE " + "(setup LIKE 'SQUEEZE%' OR setup LIKE 'AUTOSIG%') " "AND entry_time >= strftime('%s','2026-08-05 07:00:00')"), - "need": 12, "unit": "Squeeze-Trades seit dem Umbau (05.08. 08:55)", + "need": 12, "unit": "Bot-Trades (Squeeze+Signal) seit dem Umbau (05.08. 08:55)", "cmd": "python analyze_squeeze_entry_gap.py 60 → Der Median-Abstand muss " "gegen 0 gehen und die Market-Log-Zeilen ('AUTO-SQUEEZE-ENTRY') " "sollten weitgehend verschwinden. Bleibt der Abstand bei ~0,3×ATR, " diff --git a/web/app.js b/web/app.js index a0ae1b7..d72d678 100644 --- a/web/app.js +++ b/web/app.js @@ -888,6 +888,25 @@ function render(d) { if (t) { calEl.className = "news-conflict warn"; calEl.textContent = t; } else { calEl.className = "news-conflict hidden"; calEl.textContent = ""; } } + // Auto-Flip-Close sichtbar machen (2026-08-05). Dieser Exit schliesst echte + // Positionen, ist aber in BEIDEN Halbjahren gemessen NEGATIV (−92/−160 R, + // backtest_flipclose2.py) und lief bis heute voellig ohne Anzeige. Die Zeile + // nennt bewusst die Messung mit — sonst sieht ein Zaehler nach Erfolg aus. + const flipEl = $("flip-note"); + if (flipEl) { + if (d.flip_close) { + const n = d.flip_close_count || 0; + const r = d.flip_close_min_r; + flipEl.className = "news-conflict warn"; + flipEl.textContent = `🔄 Auto-Flip-Close AKTIV (ab ${r != null ? r : "?"}×ATR im Plus)` + + ` — bisher ${n}× ausgelöst · ⚠ gemessen in beiden Halbjahren negativ`; + flipEl.title = "Schliesst die Position, wenn die Empfehlung dreht. " + + "backtest_flipclose2.py: −92 R (H1) / −160 R (H2) gegenueber Nichtstun. " + + "Laeuft auf ausdruecklichen Wunsch; abschalten via [trading] auto_flip_close=false."; + } else { + flipEl.className = "news-conflict hidden"; flipEl.textContent = ""; + } + } // Schalter-Zustand: Auto-Close am S/R an/aus + Mindestgewinn const srBtn = $("srclose-btn"); if (srBtn) { diff --git a/web/index.html b/web/index.html index eb17717..8d473d6 100644 --- a/web/index.html +++ b/web/index.html @@ -6,7 +6,7 @@ Oil · MT5 - + @@ -102,6 +102,12 @@ + +
Setup
Lauf
@@ -281,6 +287,6 @@ - +