Files
AH-Oil-Trader/backtest_squeeze_entry.py
Axel HocksandClaude Opus 5 3b00820c15 Loesung fuer Fall 7 gemessen: ruhende Stop-Order am Level statt Market
Anstoss aus der Web-Recherche ("don't chase breakouts with market orders;
place orders just beyond the breakout level"). Variante E in
backtest_squeeze_entry.py: PENDING BUY_STOP/SELL_STOP, die schon liegt
waehrend der Squeeze armed ist, und vom Ausbruch selbst ausgeloest wird -
anders als die Limit-Order wartet sie NICHT auf einen Ruecklauf.

Ergebnis (80k M5, 2 Halbjahre, sequentiell, kanonischer Exit, Echtkosten):
  Level +0,05 xATR  -> OeR +0,397 / +0,553  PF 2,28 / 3,06
  Level +0,10 xATR  -> OeR +0,355 / +0,509
  Level +0,20 xATR  -> OeR +0,244 / +0,426  PF 1,62 / 2,25
gegen Market/Live -0,158 / -0,006. Selbst mit dem ~9-fachen Median-Spread
als unterstellter Fill-Slippage bleibt es beidhaelftig klar positiv.

Kernpunkt: nicht der Median-Abstand toetet den Edge, sondern der SCHWANZ
(live Median +0,306 xATR, Mittel +0,741, Max +4,339). Eine Stop-Order
deckelt den Einstiegspreis konstruktiv, die Market-Order auf dem 5-s-Poll
nicht. Die Extremfaelle stammen aus blockiert-dann-freigegebenen Ausbruechen.

NICHT gebaut - die Guards (Nacht/News/Cooldown/flat) wirken heute als
Laufzeit-Checks und muessten mit einer ruhenden Order ueber Platzieren/
Stornieren durchgesetzt werden; dazu MT5-Mindestabstand und Storno beim
Klaeren des Squeeze.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 08:49:49 +02:00

268 lines
12 KiB
Python
Raw Permalink 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 teuer ist der SPÄTE Squeeze-Einstieg — und hilft eine Abstands-Bremse?
ANLASS (2026-08-05, User): „Der Auto-Trade wurde am höchsten Punkt einer
aufsteigenden Kerze geschlossen und ein neuer Long eröffnet. Aus meiner Sicht ist
das nicht klug." Realfall 08:33:20 — Ausbruchs-Level **75,757**, Einstieg
**75,822** = **0,28×ATR darüber**, oberhalb des Hochs (75,808) der laufenden
M5-Kerze; die Kerze schloss dann bei 75,693.
⚠ DER VERDACHT IST EIN DEPLOYMENT-DRIFT-KANDIDAT, kein Bauchgefühl:
`backtest_breakout_squeeze.py` hat den Squeeze mit Einstieg **AM LEVEL** validiert
(+0,14…+0,23 ØR). Live läuft `_check_auto_squeeze` alle ~5 s und schickt eine
MARKET-Order — der Kurs ist dann schon ein Stück über dem Level. Gemessen unter X,
betrieben unter Y: dieselbe Klasse wie die fünf dokumentierten Fälle.
GEMESSEN WIRD DESHALB
A) Einstieg AM LEVEL (die Backtest-Annahme)
B) Einstieg am BAR-CLOSE (realistisch: so weit ist der Kurs gelaufen, wenn
`_squeeze_one` den Ausbruch das erste Mal sieht)
C) B + Abstands-Bremse: Ausbruch überspringen, wenn der Kurs bereits mehr als
X×ATR über/unter dem Level steht („zu spät, der Zug ist weg")
D) B + Bremse, aber die übersprungenen Ausbrüche NACHTRÄGLICH prüfen — steigt
man nur später ein oder gar nicht? (Hier: gar nicht, wie live.)
VORAB FIXIERTE ENTSCHEIDUNGSREGEL:
Eine Bremse X wird nur empfohlen, wenn sie B in **ΣR und ØR in BEIDEN Hälften**
schlägt UND die beiden Nachbar-Schwellen ebenfalls besser als B sind.
Sequentiell mit EINEM Positions-Slot (wie live), kanonischer Exit, Echtkosten.
Aufruf: python backtest_squeeze_entry.py [bars]
"""
import sys
import MetaTrader5 as mt5
from backtest_cost_gate import _atr_series, _berlin, kandidaten, kennzahlen
from core.exit_model import LIVE, simulate
_SCHWELLEN = (0.10, 0.15, 0.20, 0.30, 0.40)
def lauf(K, H, L, C, lo, hi, am_level: bool, max_abstand: float | None):
"""Sequentiell, EIN Slot. `am_level`=True → Backtest-Annahme, sonst Bar-Close.
`max_abstand` in ×ATR → Ausbruch überspringen, wenn der Kurs schon weiter ist."""
rs, abst = [], []
frei_ab = lo
for k in K:
i = k["i"]
if not (lo <= i < hi) or i < frei_ab:
continue
d, lvl, atr = k["d"], k["lvl"], k["atr"]
weg = (C[i] - lvl) * d / atr # wie weit ist der Kurs schon drüber?
if max_abstand is not None and weg > max_abstand:
continue # zu spät → gar nicht einsteigen
entry = lvl if am_level else C[i]
r, xb = simulate(entry, d, atr, H, L, C, i + 1, LIVE,
timestop=True, use_tp=True, ret_bar=True)
# Kosten immer auf den ATR bezogen; beim Bar-Close kommt der bereits
# gelaufene Weg als echter Nachteil über den schlechteren `entry` rein.
rs.append(r - k["kosten"])
abst.append(weg)
frei_ab = xb + 1
return rs, abst
def lauf_limit(K, H, L, C, lo, hi, warte: int, puffer: float = 0.0):
"""Variante D — LIMIT am Ausbruchs-Level statt Market.
Die Order liegt `warte` Bars lang am Level (+`puffer`×ATR in Trade-Richtung
zurückversetzt = etwas günstiger). Kommt der Kurs zurück → Fill zum Limit-Preis;
kommt er nicht zurück → **kein Trade** (der Zug ist weg, das ist der Preis).
Damit wird Variante A überhaupt erst erreichbar: dort wird der Level-Preis
einfach unterstellt, obwohl der Kurs ihn schon verlassen hat."""
rs = []
frei_ab = lo
verpasst = 0
for k in K:
i = k["i"]
if not (lo <= i < hi) or i < frei_ab:
continue
d, atr = k["d"], k["atr"]
limit = k["lvl"] - d * puffer * atr
fill = None
for j in range(i + 1, min(i + 1 + warte, len(C) - 1)):
# LONG: Limit unter dem Kurs → Fill, wenn das Bar-Tief es erreicht
if (L[j] <= limit) if d > 0 else (H[j] >= limit):
fill = j
break
if fill is None:
verpasst += 1
continue
r, xb = simulate(limit, d, atr, H, L, C, fill + 1, LIVE,
timestop=True, use_tp=True, ret_bar=True)
rs.append(r - k["kosten"])
frei_ab = xb + 1
return rs, verpasst
def lauf_stop(K, H, L, C, lo, hi, slip: float):
"""Variante E — ruhende STOP-Order am Ausbruchs-Level.
Sie liegt bereits, während der Squeeze `armed` ist, und wird vom Ausbruch selbst
ausgelöst → Fill am Level + `slip`×ATR (Auslösung erfolgt zum Markt, etwas
Slippage bleibt). Kein Warten auf einen Rücklauf wie bei der Limit-Order, also
auch KEINE verpassten Ausbrüche."""
rs = []
frei_ab = lo
for k in K:
i = k["i"]
if not (lo <= i < hi) or i < frei_ab:
continue
d, atr = k["d"], k["atr"]
entry = k["lvl"] + d * slip * atr
r, xb = simulate(entry, d, atr, H, L, C, i + 1, LIVE,
timestop=True, use_tp=True, ret_bar=True)
rs.append(r - k["kosten"])
frei_ab = xb + 1
return rs, 0
def zeile(lbl, k1, k2, extra=""):
def f(k):
if not k:
return f"{'—':>34}"
return (f"{k['n']:>5} {k['wr']:>5.1f}% {k['oer']:>+7.3f} "
f"{k['pf']:>5.2f} {k['sr']:>+8.1f}")
print(f" {lbl:<30}{f(k1)} |{f(k2)} {extra}")
def main():
n_bars = int(sys.argv[1]) if len(sys.argv) > 1 else 80000
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, n_bars)
point = mt5.symbol_info(sym).point
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]; T = [int(b["time"]) for b in bars]
SP = [float(b["spread"]) * point for b in bars]
A = _atr_series(H, L, C)
N = len(C); mid = N // 2
print("=" * 104)
print(f" SQUEEZE-EINSTIEG: am Level vs. hinterhergelaufen — {sym} M5, {N} Bars")
print(f" {_berlin(T[0]):%d.%m.%Y} bis {_berlin(T[-1]):%d.%m.%Y} · 2 Halbjahre · "
f"sequentiell (EIN Slot) · kanonischer Exit · Echtkosten")
print("=" * 104)
K = kandidaten(H, L, C, T, SP, A)
# Wie weit läuft der Kurs typischerweise, bis der Ausbruch sichtbar ist?
weg = sorted((C[k["i"]] - k["lvl"]) * k["d"] / k["atr"] for k in K)
print(f" Abstand Kurs↔Level beim ERSTEN Sichtbarwerden des Ausbruchs (×ATR):")
print(f" Median {weg[len(weg)//2]:.2f} · 75 % {weg[3*len(weg)//4]:.2f} · "
f"90 % {weg[int(0.9*len(weg))]:.2f} · Max {weg[-1]:.2f} (n={len(weg)})")
print(f" Der Realfall vom 05.08. lag bei 0,28×ATR — "
f"{100.0*sum(1 for w in weg if w <= 0.28)/len(weg):.0f} % der Fälle sind näher dran.")
print()
print(f" {'Variante':<30}{'H1 n WR OeR PF SigmaR':>34}"
f" |{'H2 n WR OeR PF SigmaR':>34}")
print(" " + "-" * 100)
erg = {}
for lbl, am_level, mx in (("A Einstieg AM LEVEL", True, None),
("B Einstieg am Bar-Close", False, None)):
r1, _ = lauf(K, H, L, C, 0, mid, am_level, mx)
r2, _ = lauf(K, H, L, C, mid, N, am_level, mx)
erg[lbl] = (kennzahlen(r1), kennzahlen(r2))
zeile(lbl, *erg[lbl])
print(" " + "-" * 100)
for x in _SCHWELLEN:
lbl = f"C Bremse: max {x:.2f}xATR"
r1, _ = lauf(K, H, L, C, 0, mid, False, x)
r2, _ = lauf(K, H, L, C, mid, N, False, x)
erg[lbl] = (kennzahlen(r1), kennzahlen(r2))
zeile(lbl, *erg[lbl])
print(" " + "-" * 100)
for warte, puf in ((3, 0.0), (6, 0.0), (12, 0.0), (6, 0.1), (12, 0.1)):
lbl = f"D Limit am Level {warte}B" + (f" +{puf:.1f}" if puf else "")
r1, v1 = lauf_limit(K, H, L, C, 0, mid, warte, puf)
r2, v2 = lauf_limit(K, H, L, C, mid, N, warte, puf)
erg[lbl] = (kennzahlen(r1), kennzahlen(r2))
zeile(lbl, *erg[lbl], extra=f"verpasst {v1}/{v2}")
# ── Variante E: RUHENDE STOP-ORDER am Level ──────────────────────────
# Aus der Web-Recherche (2026-08-05): „don't chase breakouts with market orders;
# place orders just beyond the breakout level." Genau das ist hier der Hebel —
# eine PENDING BUY_STOP/SELL_STOP, die schon liegt, WÄHREND der Squeeze `armed`
# ist, füllt am Level statt 0,31×ATR später. Anders als die Limit-Order (D)
# wartet sie NICHT auf einen Rücklauf: sie wird vom Ausbruch selbst ausgelöst.
# ⚠ Eine ausgelöste Stop-Order füllt zum Markt → etwas Slippage bleibt. Deshalb
# mit mehreren realistischen Aufschlägen gerechnet, nicht mit dem Idealpreis.
print(" " + "-" * 100)
for slip in (0.00, 0.05, 0.10, 0.20):
lbl = f"E Stop-Order Level +{slip:.2f}"
r1, _ = lauf_stop(K, H, L, C, 0, mid, slip)
r2, _ = lauf_stop(K, H, L, C, mid, N, slip)
erg[lbl] = (kennzahlen(r1), kennzahlen(r2))
zeile(lbl, *erg[lbl], extra="(= Variante A)" if slip == 0 else "")
a1, a2 = erg["A Einstieg AM LEVEL"]
b1, b2 = erg["B Einstieg am Bar-Close"]
print()
print("=" * 104)
print(" BEFUND 1 — was kostet das Hinterherlaufen? (A gegen B)")
print("=" * 104)
print(f" OeR H1 {a1['oer']:+.3f} -> {b1['oer']:+.3f} ({b1['oer']-a1['oer']:+.3f})"
f" H2 {a2['oer']:+.3f} -> {b2['oer']:+.3f} ({b2['oer']-a2['oer']:+.3f})")
print(f" SigmaR H1 {a1['sr']:+.1f} -> {b1['sr']:+.1f} ({b1['sr']-a1['sr']:+.1f})"
f" H2 {a2['sr']:+.1f} -> {b2['sr']:+.1f} ({b2['sr']-a2['sr']:+.1f})")
print(" ⚠ B ist der LIVE-Fall. Weicht es stark ab, gilt die validierte")
print(" Squeeze-Kennzahl (+0,14…+0,23 OeR) fuer den Live-Betrieb NICHT.")
print()
print("=" * 104)
print(" BEFUND 2 — hilft eine Abstands-Bremse? (Regel VOR dem Lauf fixiert:")
print(" SigmaR UND OeR in BEIDEN Haelften > B, plus beide Nachbarn)")
print("=" * 104)
def schlaegt(x):
k1, k2 = erg[f"C Bremse: max {x:.2f}xATR"]
return (k1 and k2 and k1["sr"] > b1["sr"] and k2["sr"] > b2["sr"]
and k1["oer"] > b1["oer"] and k2["oer"] > b2["oer"])
treffer = []
for idx, x in enumerate(_SCHWELLEN):
nb = [_SCHWELLEN[j] for j in (idx - 1, idx + 1) if 0 <= j < len(_SCHWELLEN)]
selbst, nb_ok = schlaegt(x), all(schlaegt(m) for m in nb)
if selbst and nb_ok:
treffer.append(x)
print(f" X = {x:.2f}xATR "
+ ("BESTEHT" if (selbst and nb_ok) else
"schlaegt B, Nachbarn NICHT" if selbst else "faellt durch"))
print()
if treffer:
print(f" => Kandidat(en): {', '.join(f'{x:.2f}' for x in treffer)}")
else:
print(" => KEINE Schwelle besteht. Eine Abstands-Bremse ist nicht belegt —")
print(" uebersprungene Ausbrueche waeren im Schnitt NICHT schlechter.")
print()
print("=" * 104)
print(" BEFUND 3 — schlaegt eine LIMIT-Order am Level den Market-Einstieg?")
print(" (Regel: SigmaR UND OeR in BEIDEN Haelften > B, Nachbarn ebenso)")
print("=" * 104)
d_treffer = []
d_keys = [k for k in erg if k.startswith("D ")]
for k in d_keys:
k1, k2 = erg[k]
gut = (k1 and k2 and k1["sr"] > b1["sr"] and k2["sr"] > b2["sr"]
and k1["oer"] > b1["oer"] and k2["oer"] > b2["oer"])
if gut:
d_treffer.append(k)
print(f" {k:<30} {'BESSER als Market' if gut else 'faellt durch'}")
print()
if d_treffer:
print(f" => {len(d_treffer)} von {len(d_keys)} Limit-Varianten schlagen den")
print(" Market-Einstieg in beiden Haelften.")
else:
print(" => Auch die Limit-Order rettet es nicht.")
print()
if __name__ == "__main__":
main()