Files
AH-Oil-Trader/backtest_squeeze_woche.py
T
Axel HocksandClaude Opus 5 35e91474e8 Auto-Squeeze Zuverlaessigkeitspruefung + Wochen-Simulation
Befund 0 beantwortet die Frage vorweg: der Trader ist AUS.
runtime_state.json enthaelt "auto_squeeze": false und ueberschreibt
die ini (true) beim Start - dieselbe Falle wie am 31.07. bei
auto_signal, nur mit umgekehrtem Vorzeichen. Letzter Squeeze-Trade
06.08. 13:52, entry_count 0. Der Config-Waechter meldet es korrekt.

Befund 1: die Mechanik arbeitet. Die Simulation nutzt die echte
_squeeze_one und reproduziert 10 von 11 Live-Trades binnen 10 min
auf wenige Cent genau.

Befund 2: Woche n=47, Treffer 57 %, OeR +0,196, PF 1,37, SumR +9,20
(~+610 EUR bei 2,5 Lots) - im validierten Band. ABER: die Sim
ueberhandelt um das 2,4-fache (kein Live-Dedup/Cooldown), der Edge
stirbt bei 0,20xATR Fill-Slippage, und eine Woche ist Rauschen -
die Phase mit AUSgeschaltetem Trader war sogar die bessere.

Befund 3: auf denselben 10 Einstiegen war der LIVE-Exit 70 EUR
BESSER als der kanonische (-109,52 gegen -179,27). Das widerspricht
dem B4-Befund vom 01.08. ("die Luecke entsteht im Exit"). Der
S/R-Close rettete drei Trades. n=10, gemischt - kein Urteil, aber
ein Warnschild gegen den umgekehrten Schluss.

Befund 4: B5 bei 11/20 Trades, beide Bedingungen weit verletzt
(Verhaeltnis 0,17, PF 0,45 bei 73 % Trefferquote). Formal nicht
faellig.

Offen: zwei Live-BUYs am 06.08. 13:51/13:52 zum identischen Preis
auf einen Ausbruch - moeglicher Doppel-Einstieg, ungeprueft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 09:01:34 +02:00

170 lines
6.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
"""Haette der Auto-Squeeze in der letzten Woche Gewinn gemacht?
ANLASS (2026-08-11, User: „analysiere den Breakout-Auto-Trader, ob er zuverlaessig
funktioniert; simuliere ueber die letzte Woche, ob er Gewinn erzielt haette").
⚠⚠ WAS DIESE SIMULATION IST UND WAS NICHT. **Eine Woche ist KEIN Edge-Nachweis.**
Der Squeeze feuert 23×/Woche; das sind Stichproben von n≈26, und im Projekt sind
Einzelzeitraeume schon dreimal faelschlich zum Beleg erhoben worden (15-Minuten-
Regel 31.07., Nacht-Guard 05.08., das Veto im Wochentest 08.08.). Gemessen wird
hier die **MECHANIK**: feuert die Regel ueberhaupt, zu welchen Preisen, und was
haette der kanonische Exit daraus gemacht.
MODELLIERT WIRD DER LIVE-PFAD (Stand seit 05.08.):
· echte Live-Funktion `WaveRecommender._squeeze_one` — kein Nachbau
· **ruhende Stop-Order** an der Box-Grenze, solange `armed`; Fuellung bei
BERUEHRUNG (nicht bei Bar-Schluss). Das ist der teurere und richtige Fall:
(a) „Close bricht Level" ist Look-ahead und rund +0,32 R zu optimistisch
(`backtest_squeeze_touchfill.py`).
· Nacht-Sperre 36 Uhr Berlin (`auto_squeeze_night_hours`)
· ATR-Floor, EIN Positions-Slot (sequentiell), Echtkosten aus dem Bar-Spread
· **kanonischer Exit** `core.exit_model.LIVE` — ausdruecklich NICHT
`squeeze_scan.exit_legacy` (Trail 1,5 · kein Lock · kein Time-Stop · kein TP),
gegen den der B4-Monitor bis heute misst.
Aufruf: python backtest_squeeze_woche.py [tage] (Default 7)
"""
from __future__ import annotations
import sys
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo
import MetaTrader5 as mt5
from core.exit_model import LIVE, simulate
from core.wave_rec import WaveRecommender
_BERLIN = ZoneInfo("Europe/Berlin")
_BROKER = timezone(timedelta(hours=3))
_NACHT = {3, 4, 5, 6}
_ATR_MIN = 0.06
_SLIPPAGE = (0.0, 0.05, 0.10, 0.20) # ×ATR Aufschlag auf den Fill
def _berlin(ts):
return datetime.fromtimestamp(ts, timezone.utc).replace(
tzinfo=_BROKER).astimezone(_BERLIN)
def atr_series(H, L, C, p=14):
tr = [0.0]
for i in range(1, len(C)):
tr.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1])))
out = [None]
for i in range(1, len(C)):
seg = tr[max(1, i - p + 1):i + 1]
out.append(sum(seg) / len(seg))
return out
def lauf(T, H, L, C, SP, A, lo_i, slip=0.0, nacht=True):
"""Sequentiell EIN Slot, ruhende Stop-Order, Fuellung bei Beruehrung."""
trades = []
i = lo_i
n = len(C)
while i < n - LIVE.max_hold - 2:
atr = A[i]
if not atr or atr < _ATR_MIN:
i += 1; continue
if nacht and _berlin(T[i]).hour in _NACHT:
i += 1; continue
sq = WaveRecommender._squeeze_one(H[:i + 1], L[:i + 1], C[:i + 1], atr)
if not sq or sq.get("box_hi") is None or sq.get("state") is None:
i += 1; continue
# ⚠ Die Box stammt aus den N bereits GESCHLOSSENEN Bars vor i — eine
# ruhende Order darf waehrend Bar i also legitim gefuellt werden.
up = sq["box_hi"] + 0.1 * atr
dn = sq["box_lo"] - 0.1 * atr
traf_up = H[i] >= up
traf_dn = L[i] <= dn
if not (traf_up or traf_dn):
i += 1; continue
if traf_up and traf_dn:
# Beide Seiten in EINEM Bar — aus OHLC nicht aufloesbar. Die naeher
# am Open liegende Seite wird zuerst beruehrt worden sein.
traf_up = abs(up - C[i - 1]) <= abs(C[i - 1] - dn)
traf_dn = not traf_up
d = 1 if traf_up else -1
fill = (up + slip * atr) if traf_up else (dn - slip * atr)
kost = (SP[i] if SP[i] > 0 else 0.0225) / atr
R, xb = simulate(fill, d, atr, H, L, C, i + 1, LIVE, ret_bar=True)
trades.append({"i": i, "t": T[i], "d": d, "fill": fill, "atr": atr,
"R": R - kost, "bars": xb - i,
"box_atr": sq.get("box_atr")})
i = xb + 1 # Slot war belegt
return trades
def kz(tr, lbl):
if not tr:
return f" {lbl:<34} keine Ausbrueche"
n = len(tr); Rs = [x["R"] for x in tr]
w = sum(1 for x in Rs if x > 0)
g = sum(x for x in Rs if x > 0); v = -sum(x for x in Rs if x < 0)
ow = g / w if w else 0.0
ol = v / (n - w) if n - w else 0.0
return (f" {lbl:<34} n={n:>3} Treffer {100*w/n:>3.0f} % OeR {sum(Rs)/n:>+6.3f} "
f"PF {(g/v if v else 9.99):>4.2f} Verh {(ow/ol if ol else 9.99):>4.2f} "
f"SumR {sum(Rs):>+6.2f}")
def main():
tage = int(sys.argv[1]) if len(sys.argv) > 1 else 7
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, 20000)
point = mt5.symbol_info(sym).point
mt5.shutdown()
T = [int(b["time"]) for b in bars]
H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars]
C = [float(b["close"]) for b in bars]
SP = [float(b["spread"]) * point for b in bars]
A = atr_series(H, L, C)
grenze = T[-1] - tage * 86400
lo_i = next(i for i, t in enumerate(T) if t >= grenze)
lo_i = max(lo_i, 60)
print("=" * 100)
print(f" AUTO-SQUEEZE — Simulation der letzten {tage} Tage ({sym})")
print(f" {_berlin(T[lo_i]):%d.%m %H:%M} bis {_berlin(T[-1]):%d.%m %H:%M} "
f"{len(C)-lo_i} M5-Bars")
print(" Live-Pfad: ruhende Stop-Order an der Box, Fuellung bei BERUEHRUNG,")
print(" Nacht 3-6 gesperrt, EIN Slot, kanonischer Exit, Echtkosten.")
print("=" * 100)
basis = lauf(T, H, L, C, SP, A, lo_i)
print("\n ── Fill-Slippage (eine Stop-Order fuellt zum Markt, nicht zum Ideal) ──")
for s in _SLIPPAGE:
print(kz(lauf(T, H, L, C, SP, A, lo_i, slip=s), f"Slippage +{s:.2f}xATR"))
print("\n ── Gegenprobe: ohne Nacht-Sperre ──")
print(kz(lauf(T, H, L, C, SP, A, lo_i, nacht=False), "alle Stunden"))
print(f"\n ── die einzelnen Ausbrueche (Slippage 0) ──")
if not basis:
print(" KEINER — die Regel hat in diesem Fenster nicht gefeuert.")
for x in basis:
print(f' {_berlin(x["t"]):%d.%m %H:%M} {"LONG " if x["d"]>0 else "SHORT"} '
f'@ {x["fill"]:.3f} Box {x["box_atr"]:.2f}xATR '
f'{x["bars"]*5:>4} min R {x["R"]:>+6.2f}')
if basis:
# EUR-Naeherung: R x ATR x Lots x ~87,6 EUR/$/Lot (robust geschaetzt, s. CLAUDE.md)
lots = 2.5
eur = sum(x["R"] * x["atr"] for x in basis) * lots * 87.6
print(f"\n grob in EUR bei {lots} Lots: {eur:>+8.0f} EUR")
print(" ⚠ Naeherung — die echte Lot-Groesse schwankt mit dem Konto.")
print("\n " + "-" * 96)
print(" ⚠⚠ EINE WOCHE IST KEIN EDGE-NACHWEIS. Der Squeeze feuert 2-3x/Woche;")
print(" n dieser Groesse ist Rauschen. Die 2-Stichproben-Regel gilt weiter.")
return 0
if __name__ == "__main__":
raise SystemExit(main())