Files
AH-Oil-Trader/analyze_squeeze_entry_gap.py
T
Axel HocksandClaude Opus 5 5f70980e59 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>
2026-08-05 09:06:50 +02:00

154 lines
6.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
_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()