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 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-05 09:06:50 +02:00
co-authored by Claude Opus 5
parent 5e0e8b00ca
commit 5f70980e59
5 changed files with 201 additions and 10 deletions
+153
View File
@@ -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: <dir> @ Ausbruch <level>"), 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()