Files
AH-Oil-Trader/analyze_squeeze_entry_gap.py
T
Axel HocksandClaude Opus 5 516f1eaa7f Roast-Punkte 1-3: Erfolgskontrolle erweitert, flip_close sichtbar, WARTEN-Massstab
korrigiert - und ein eigener Messfehler gefunden

(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 <noreply@anthropic.com>
2026-08-05 12:45:36 +02:00

176 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
# ⚠ 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):
"""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, 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_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(); sig_fallback.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: 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_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_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} "
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()