Files
AH-Oil-Trader/analyze_squeeze_entry_gap.py
T
Axel HocksandClaude Opus 5 bf838b4d03 analyze_squeeze_entry_gap: nur noch echte Bot-Trades zaehlen
Das Skript zaehlte im Abschnitt "per Pending gefuellt" JEDEN Trade seit dem
Umbau — es meldete "31 Trades, davon 30 ohne Market-Log-Zeile". Real waren
es 5 Bot-Trades (1 Market-Fallback + 4 Pending-Fills); der Rest waren
manuelle. Moeglich war die Verwechslung genau deshalb, weil ein Pending-Fill
bis zum Fix von heute ohne `setup` gebucht wurde.

Jetzt Filter auf SQUEEZE_*/AUTOSIG_* und Setup in der Ausgabe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:57:52 +02:00

185 lines
8.5 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, setup "
"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
# ⚠ NUR Bot-Trades. Bis zum Fix am 06.08. zaehlte hier JEDER Trade mit —
# ein Pending-Fill wurde damals ohne `setup` gebucht (er laeuft nicht durch
# `engine._open`), also war er von einem manuellen nicht zu unterscheiden.
# Ergebnis: "31 Trades, davon 30 ohne Log-Zeile" — real waren es 5 Bot-Trades.
# Seit `engine._check_pending_fill` traegt der Bot das Setup nach.
su = (t["setup"] or "")
if not (su.startswith("SQUEEZE") or su.startswith("AUTOSIG")):
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" Bot-Trades seit dem Umbau: {n_bot} · davon per Pending gefüllt "
f"(keine Market-Log-Zeile): {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['setup'] or '?'):<14} "
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()