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>
176 lines
7.9 KiB
Python
176 lines
7.9 KiB
Python
#!/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()
|