From 5f70980e59ff8b8709a3a7c174a28e6515d78175 Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Wed, 5 Aug 2026 09:06:50 +0200 Subject: [PATCH] Erfolgskontrolle fuer den Stop-Order-Umbau: Skript + Reminder analyze_squeeze_entry_gap.py fuehrt die zwei noetigen Quellen zusammen - das Ausbruchs-Level steht NUR im Log, der tatsaechliche Fill NUR in trades - und splittet am Umbau-Stichtag. Die Erfolgsmeldung ist paradox: eine per Pending gefuellte Order erzeugt KEINE AUTO-SQUEEZE-ENTRY-Logzeile (die entsteht nur im Market-Fallback), Squeeze-Trades ohne Log-Treffer sind also der Erfolg. Das Skript weist sie deshalb getrennt aus. Als Messung squeeze_entry_gap im measurement_reminder.py hinterlegt (>=12 Squeeze-Trades ab dem Umbau). Stichtag exakt 05.08. 08:55 statt Mitternacht - der 08:33-Trade lief noch ueber die Market-Order und haette die Zaehlung verfaelscht (0/12 statt faelschlich 1/12). Zahlen-Korrektur: die zuerst dokumentierten +0,306 / +0,741 xATR stammten aus einer Scratchpad-Auswertung mit FESTEM Stundenversatz. DST-korrekt ueber zoneinfo sind es +0,275 / +0,686 (n=34 unveraendert). In CLAUDE.md, engine.py, trader.py und der ini nachgezogen; die Schlussfolgerung aendert sich nicht. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 30 +++++-- analyze_squeeze_entry_gap.py | 153 +++++++++++++++++++++++++++++++++++ core/engine.py | 2 +- core/trader.py | 2 +- measurement_reminder.py | 24 ++++++ 5 files changed, 201 insertions(+), 10 deletions(-) create mode 100644 analyze_squeeze_entry_gap.py diff --git a/CLAUDE.md b/CLAUDE.md index 3b2662b..d5b7de7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1466,8 +1466,8 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: **`backtest_breakout_squeeze.py` steigt AM LEVEL ein** (`sim(lvl, …)`). Live schickt `_check_auto_squeeze` eine **MARKET**-Order, sobald `state=='active'` — der Kurs ist dann schon ein Stück weiter. **Aus 34 echten Live-Entries (Logs 16.07.–05.08., - Level aus der Log-Zeile × Einstiegspreis aus der DB): Median +0,306×ATR über dem - Level**, Mittel +0,741 · 75 % +0,863 · Max +4,339; nur **18 %** liegen innerhalb + Level aus der Log-Zeile × Einstiegspreis aus der DB): Median +0,275×ATR über dem + Level**, Mittel +0,686 · 75 % +0,744 · Max +4,339; nur **18 %** liegen innerhalb 0,10×ATR, **53 % über 0,30×ATR**. **Was dieser Preisunterschied kostet** (80k M5, 2 Halbjahre, sequentiell EIN Slot, kanonischer Exit, Echtkosten): @@ -1477,7 +1477,7 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: | **B Einstieg am Bar-Close** (≈ live) | **−0,158 / −125,9** | **−0,006 / −7,6** | 37/42 % | **Allein der Einstiegspreis dreht das Setup — −0,61 ØR in BEIDEN Hälften, WR bricht von 56 auf 37 %.** Modell B ist dabei **nicht pessimistisch**: sein Median-Abstand - (0,38×ATR) liegt nah am LIVE gemessenen (0,31×ATR); der Live-Betrieb liegt also + (0,38×ATR) liegt nah am LIVE gemessenen (0,28×ATR); der Live-Betrieb liegt also ~80 % des Weges von A nach B, also grob bei **breakeven bis leicht negativ** — nicht bei den dokumentierten +0,14…+0,23. ⚠⚠ **Das ordnet den B4-Befund vom 01.08. NEU ein.** Dort stand: „Mechanik +0,102 vs. @@ -1520,7 +1520,7 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: **Selbst mit 0,20×ATR unterstellter Fill-Slippage — dem ~9-fachen des Median-Spreads — bleibt es in BEIDEN Hälften klar positiv.** ⚠⚠ **Der Schlüssel ist nicht der Median, sondern der SCHWANZ.** Live ist der - Median-Abstand +0,306×ATR, der **Mittelwert aber +0,741** (Max +4,339). Eine + Median-Abstand +0,275×ATR, der **Mittelwert aber +0,686** (Max +4,339). Eine Stop-Order **deckelt den Einstiegspreis konstruktiv**; die Market-Order auf dem 5-s-Poll tut das nicht. Die Extremfälle (+3,5 / +4,3×ATR) stammen erkennbar aus **blockiert-dann-freigegebenen** Ausbrüchen: feuert der Squeeze, während eine @@ -1560,9 +1560,23 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: Live verifiziert am Snapshot: `squeeze_pending=true`, `squeeze_max_chase_atr=0.2`, `squeeze_pending_levels` (leer, weil eine Position offen war = korrektes Verhalten). **Zurück zum alten Verhalten: `squeeze_pending_entry=false`.** - ⚠ **B4-Pflicht:** der Live-Abstand Einstieg↔Level muss jetzt gegen ~0 gehen - (vorher Median +0,306×ATR). Das ist die Erfolgskontrolle — nachrechnen wie in - diesem Abschnitt (Log-Zeile „Ausbruch " × `trades.entry_price`). + ⚠ **B4-Pflicht + Werkzeug:** der Live-Abstand Einstieg↔Level muss jetzt gegen ~0 + gehen (vorher Median +0,275×ATR). Dafür gibt es jetzt + **`analyze_squeeze_entry_gap.py`** (führt die zwei nötigen Quellen zusammen: das + Level steht NUR im Log, der Fill NUR in `trades`) — mit Vorher/Nachher-Split am + Umbau-Stichtag. ⚠ **Die Erfolgsmeldung ist paradox:** eine per Pending gefüllte + Order erzeugt **keine** `AUTO-SQUEEZE-ENTRY`-Logzeile (die entsteht nur im + Market-Fallback). Squeeze-Trades **ohne** Log-Treffer sind also der Erfolg, nicht + ein Fehler — das Skript weist sie deshalb getrennt aus. Überwacht als Messung + `squeeze_entry_gap` im `measurement_reminder.py` (≥12 Squeeze-Trades ab dem + Umbau; Stichtag exakt 05.08. 08:55, nicht Mitternacht — der 08:33-Trade lief noch + über Market). Bleibt der Abstand bei ~0,3×ATR, füllt die Pending nicht → Broker- + Mindestabstand und die Logzeile „Squeeze-Pending nicht platzierbar" prüfen. + ⚠ **Zahlen-Korrektur (05.08.):** zuerst standen hier Median +0,306 / Ø +0,741 — + das war eine Scratchpad-Auswertung mit FESTEM Stundenversatz. `analyze_squeeze_ + entry_gap.py` rechnet DST-korrekt über `zoneinfo` und liefert **+0,275 / +0,686** + (n=34 unverändert). Gültig sind die zweiten Werte; die Schlussfolgerung ändert + sich nicht. - **Auto-Squeeze-Entry = LIVE AUTONOM (User-Vorgabe 2026-07-16, `[trading] auto_squeeze=true`, Default FALSE):** der Bot eröffnet **selbständig eine echte Order**, sobald der **gemessen-validierte** Squeeze-Ausbruch feuert (`wave.squeeze. @@ -3019,7 +3033,7 @@ Bedingungen BETRIEBEN als VALIDIERT. An EINEM Tag wurden drei Fälle gefunden: | 2 | `breakout_k=0,3` | **feste** Zeitebene | TF wechselt 33×/Tag, löscht den Anker | 43 % → **93 % WARTEN** | | 3 | Trailing `mult` | **1,5** | **1,5–3,0** je nach TF | H1 ΣR −249 → **−1367**, Worst −1,50 → −2,50 | | 4 | `_broker_offset_s()` | **frischem** Tick | Tick beliebig alt (Wochenende) | Offset **−9,5 h statt +3 h** → `open_time` 12,5 h in der Zukunft, **Time-Stop-Alter negativ** | -| 7 | Auto-Squeeze-**Einstiegspreis** | Fill **AM Ausbruchs-Level** (`backtest_breakout_squeeze.py`) | **MARKET**-Order, live median **+0,306×ATR** über dem Level (34 echte Entries) | ØR **+0,456 → −0,158** (H1) bzw. **+0,611 → −0,006** (H2); WR 56 → 37 % | +| 7 | Auto-Squeeze-**Einstiegspreis** | Fill **AM Ausbruchs-Level** (`backtest_breakout_squeeze.py`) | **MARKET**-Order, live median **+0,275×ATR** über dem Level (34 echte Entries) | ØR **+0,456 → −0,158** (H1) bzw. **+0,611 → −0,006** (H2); WR 56 → 37 % | | 6 | `auto_squeeze_skip_night` | Nacht-Kostenfalle am **Wellensignal** (alle Nacht-Bars: 0,333×ATR) | **Squeeze**-Ausbrüche, die nur bei anziehendem ATR feuern (0,183×ATR = Tag-Niveau) | Gate kostet ΣR **−42 (H1) / −243 (H2)**; geblockte Ausbrüche sind in beiden Hälften positiv | | 5 | `backtest_auto_signal.py` | **ohne Winkel** (`_build` ohne `angle=` → Default 90 → `ad=0`) | Winkel steuert einen **Signalzweig** (Reversal) UND **±15 Konfidenzpunkte** | Urteil vom 30.07. beschrieb weder das alte noch das neue System; der Winkelterm allein hebt H1 bei conf 75 von **−0,123 auf −0,047** | diff --git a/analyze_squeeze_entry_gap.py b/analyze_squeeze_entry_gap.py new file mode 100644 index 0000000..31b2aa5 --- /dev/null +++ b/analyze_squeeze_entry_gap.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +"""Wie weit über dem Ausbruchs-Level steigt der Auto-Squeeze LIVE wirklich ein? + +WOZU — das ist die Erfolgskontrolle für den Umbau vom 2026-08-05 (Market-Order → +ruhende Stop-Order, `squeeze_pending_entry`). Der gemessene Zusammenhang +(`backtest_squeeze_entry.py`): der Squeeze hat seinen gesamten Edge im +EINSTIEGSPREIS — am Level ØR **+0,456/+0,611**, hinterhergelaufen **−0,158/−0,006**. +Vor dem Umbau lag der Live-Abstand im **Median +0,306×ATR** (Mittel +0,741, Max ++4,339). **Er muss jetzt gegen 0 gehen.** Tut er das nicht, greift die Pending-Order +nicht wie gedacht — dann ist der Umbau wirkungslos, ohne dass es sonst auffiele. + +QUELLE (bewusst zwei unabhängige): das Ausbruchs-Level steht NUR im Log +(„AUTO-SQUEEZE-ENTRY: @ Ausbruch "), der tatsächliche Fill NUR in der +`trades`-Tabelle. Zusammengeführt über die Zeit (±90 s). +⚠ Seit dem Umbau füllt eine Pending-Order OHNE diese Log-Zeile (sie entsteht im +Market-Fallback-Pfad). Deshalb werden Squeeze-Trades ohne Log-Treffer separat +ausgewiesen — **genau die sind der Erfolg**, nicht ein Fehler. + +Aufruf: python analyze_squeeze_entry_gap.py [tage] +""" +import glob +import os +import re +import sqlite3 +import sys +from datetime import datetime, timedelta, timezone +from zoneinfo import ZoneInfo + +import MetaTrader5 as mt5 + +_HIER = os.path.dirname(os.path.abspath(__file__)) +_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.]+)") + + +def _berlin(ts): + """MT5-Bar-Zeitstempel → echte Berliner Zeit (s. CLAUDE.md, Fix 2026-08-05).""" + return (datetime.fromtimestamp(int(ts), _UTC) + .replace(tzinfo=_BROKER).astimezone(_BERLIN)) + + +def _stat(v, lbl): + if not v: + print(f" {lbl:<22} —") + return + s = sorted(v) + print(f" {lbl:<22} n={len(s):<4} Median {s[len(s)//2]:+.3f} · " + f"Ø {sum(s)/len(s):+.3f} · 75 % {s[3*len(s)//4]:+.3f} · Max {s[-1]:+.3f}") + + +def main(): + tage = int(sys.argv[1]) if len(sys.argv) > 1 else 60 + seit = datetime.now(_BERLIN) - timedelta(days=tage) + + # ── 1) Log: Ausbruchs-Level je Market-Entry ────────────────────────── + log_eintraege = [] + 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) + 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)))) + except OSError: + continue + log_eintraege.sort() + + # ── 2) ATR-Reihe für die Normierung ────────────────────────────────── + 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, 30000) + 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] + # Bar-Zeit → lokale Epoch, DST-korrekt (kein fester Versatz!) + EP = [_berlin(b["time"]).timestamp() for b in bars] + tr = [0.0] + [max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1])) + for i in range(1, len(C))] + + def atr_bei(ep): + j = min(range(len(EP)), key=lambda i: abs(EP[i] - ep)) + seg = tr[max(1, j - 13):j + 1] + return (sum(seg) / len(seg)) if seg else 0.0 + + # ── 3) Trades dazu ─────────────────────────────────────────────────── + db = sqlite3.connect(os.path.join(_HIER, "oil_widget_history.db")) + db.row_factory = sqlite3.Row + trades = db.execute( + "SELECT ticket, entry_time, entry_price, direction, pnl, closed_by " + "FROM trades WHERE entry_time >= ? ORDER BY entry_time", + (seit.timestamp(),)).fetchall() + + vorher, nachher, ohne_log = [], [], [] + getroffen = set() + for ts, richtung, lvl in log_eintraege: + ep = ts.timestamp() + kand = [t for t in trades if abs(t["entry_time"] - ep) <= 90 + and t["entry_price"]] + if not kand: + continue + t = min(kand, key=lambda r: abs(r["entry_time"] - ep)) + getroffen.add(t["ticket"]) + a = atr_bei(ep) + if a <= 0: + continue + d = 1 if richtung == "LONG" else -1 + weg = (float(t["entry_price"]) - lvl) * d / a + (nachher if ts >= _UMBAU else vorher).append(weg) + + print("=" * 82) + print(f" AUTO-SQUEEZE — Abstand Einstieg ↔ Ausbruchs-Level (×ATR), {tage} Tage") + print(f" Umbau auf ruhende Stop-Orders: {_UMBAU:%d.%m.%Y %H:%M}") + print("=" * 82) + print(" Market-Entries (haben eine Log-Zeile mit dem Level):") + _stat(vorher, "VOR dem Umbau") + _stat(nachher, "NACH dem Umbau") + print() + print(" Erwartung: NACH dem Umbau sollten hier kaum noch Entries auftauchen —") + 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 + for t in trades: + if t["entry_time"] < _UMBAU.timestamp(): + continue + n_sq += 1 + if t["ticket"] not in getroffen: + ohne_log.append(t) + print(f" Trades seit dem Umbau: {n_sq} · davon ohne Market-Log-Zeile: " + f"{len(ohne_log)}") + 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} " + f"P&L {(t['pnl'] if t['pnl'] is not None else 0):+.2f} {t['closed_by']}") + print() + print(" MASSSTAB (backtest_squeeze_entry.py): am Level ØR +0,456/+0,611 ·") + print(" bei 0,20×ATR noch +0,244/+0,426 · hinterhergelaufen −0,158/−0,006.") + print(" Vor dem Umbau war der Live-Median +0,306×ATR.") + print() + + +if __name__ == "__main__": + main() diff --git a/core/engine.py b/core/engine.py index 89cd9af..201306b 100644 --- a/core/engine.py +++ b/core/engine.py @@ -1104,7 +1104,7 @@ class TradingEngine: WARUM (gemessen `backtest_squeeze_entry.py`, 2026-08-05): der Squeeze hat seinen gesamten Edge im EINSTIEGSPREIS. Die Market-Order auf dem 5-s-Poll - füllt live im Median 0,306×ATR hinter dem Level (Mittel 0,741, Max 4,3) → + füllt live im Median 0,275×ATR hinter dem Level (Mittel 0,686, Max 4,3) → ØR kippt von +0,456 auf −0,158. Eine ruhende Stop-Order deckelt den Preis konstruktiv: selbst mit 0,20×ATR Fill-Slippage bleibt ØR +0,244/+0,426. diff --git a/core/trader.py b/core/trader.py index cde1849..078d585 100644 --- a/core/trader.py +++ b/core/trader.py @@ -199,7 +199,7 @@ class TradeManager: # ══════════════════════════════════════════════════════════════════════ # WARUM: `backtest_squeeze_entry.py` hat gemessen, dass der Squeeze seinen # gesamten Edge im EINSTIEGSPREIS hat. Die Market-Order auf dem 5-s-Poll füllt - # live im Median 0,306×ATR hinter dem Level (Mittel 0,741, Max 4,3) — damit + # live im Median 0,275×ATR hinter dem Level (Mittel 0,686, Max 4,3) — damit # dreht ØR von +0,456 auf −0,158. Eine ruhende Stop-Order deckelt den Preis # konstruktiv: selbst mit 0,20×ATR unterstellter Fill-Slippage bleibt ØR # +0,244/+0,426 in beiden Halbjahren. diff --git a/measurement_reminder.py b/measurement_reminder.py index 2d447b8..ad15b02 100644 --- a/measurement_reminder.py +++ b/measurement_reminder.py @@ -145,6 +145,30 @@ CHECKS = [ "→ `auto_squeeze=false` (B5-Rückbau, kein Ego). Sonst weiterlaufen " "lassen. Zahlen: /api/squeeze_monitor bzw. Statistik-Tab.", }, + { + "key": "squeeze_entry_gap", + "title": "Stop-Order-Umbau: kommt der Einstieg jetzt AM Level an?", + "why": "Am 05.08. wurde der Auto-Squeeze von der Market-Order auf ruhende " + "Stop-Orders am Ausbruchs-Level umgestellt. Grund: der Squeeze hat " + "seinen GESAMTEN Edge im Einstiegspreis — am Level ØR +0,456/+0,611, " + "hinterhergelaufen −0,158/−0,006 (`backtest_squeeze_entry.py`). Live " + "lag der Abstand vorher im Median +0,275×ATR (Mittel +0,686, Max " + "+4,339). Greift die Pending-Order nicht wie gedacht, ist der ganze " + "Umbau wirkungslos — und das fiele sonst NICHT auf, weil der Bot " + "weiterhin Trades macht.", + # ⚠ Stichtag exakt auf den Umbau (05.08. 08:55 Berlin = 06:55 UTC), NICHT auf + # 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%' " + "AND entry_time >= strftime('%s','2026-08-05 07:00:00')"), + "need": 12, "unit": "Squeeze-Trades 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, " + "füllt die Pending-Order nicht — dann Broker-Mindestabstand und die " + "Log-Zeile 'Squeeze-Pending nicht platzierbar' prüfen.", + }, { "key": "night_window_0127", "title": "Nacht-Fenster 3–6: tragen die freigegebenen Stunden 0/1/2/7?",