Files
AH-Oil-Trader/backtest_squeeze_srclose2.py
T
Axel HocksandClaude Opus 5 1e8d700f46 "S/R-Close kappt die Squeeze-Laeufer" = widerlegt (backtest_squeeze_srclose2.py)
Meine eigene Hypothese vom selben Tag -- gemessen falsch, und zwar doppelt.

(1) BACKTEST (80k M5, 2 Halbjahre, Einstiege ueber den geteilten
squeeze_scan.scan(on_entry=...), ruhende Order am Level mit Fill bei Beruehrung,
kanonischer Exit, Echtkosten, ein Slot; identische Entry-Liste ueber alle
Varianten, variiert wird NUR der Exit):

  A) ohne S/R-Close      H1 -0,151 / PF 0,72   H2 -0,079 / PF 0,86
  B) mit p<0,35 (LIVE)   H1 -0,119 / PF 0,76   H2 -0,049 / PF 0,91   D +0,032/+0,030
  B) mit p<0,45          H1 -0,088             H2 -0,046             D +0,063/+0,033

Der S/R-Close ist in BEIDEN Haelften besser als ohne -- die vorab fixierte Regel
sagt: anlassen. Und die von mir vorgeschlagene "Abhilfe" (Close erst ab
Mindestgewinn) macht H1 schlechter (D -0,006 / -0,011).

(2) LIVE, 11 Squeeze-Trades ab 01.08.:
  sl        5 Trades  -198,17 EUR
  sr_close  4 Trades   +15,98 EUR   <- der verdaechtigte Exit hat BEIGETRAGEN
  manual    2 Trades    +8,73 EUR
Der groesste Gewinner (+77,34) kam ebenfalls ueber sl, also ueber das Trailing.
Die Groesse steckt allein auf der Verlustseite: -175,21 EUR bei 2,91 Lots =
0,695 $ = der planmaessige 2xATR-Stop. Der Hebel zeigt auf SIZING, nicht Exit.

GRENZE, nicht ueberspielt: die ABSOLUTEN Werte reproduzieren den dokumentierten
Kontrollwert NICHT (backtest_squeeze_touchfill.py: +0,124/+0,292 bei 1.092/1.807
Trades gegen -0,151/-0,079 bei 273/422). Belastbar ist allein der RELATIVE
Vergleich; "der Squeeze ist negativ" ist hier NICHT belegt.

Zwei eigene Fehler dokumentiert, weil es Fehlerklassen sind:
- erster Lauf nahm den Modul-Default ATRMIN_STD=0,12 statt des Live-Werts 0,06
  -> H1 127 gegen H2 483 Einstiege, voellig andere Population. Dieselbe Klasse
  wie das fehlende set_entry_room(0.6): ein aufgerufenes Gate ist nicht dasselbe
  wie ein aktives Gate.
- erster Entwurf haengte den Close an stop_when (prueft den BAR-CLOSE gegen ein
  0,018-$-Fenster) -> feuerte fast nie. Live prueft den laufenden Kurs im
  Sekundentakt; das Analogon ist die Bar-SPANNE.

Nichts am Live-System geaendert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:33:11 +02:00

316 lines
14 KiB
Python

#!/usr/bin/env python3
"""Kappt der S/R-Auto-Close die Squeeze-Laeufer? — NEU unter dem HEUTIGEN Stand.
ANLASS (2026-08-13). Der Pending-Umbau vom 05.08. hat gewirkt (7 von 8 Bot-Trades
fuellen per ruhender Order), der Squeeze verliert aber weiter. Die Signatur zeigt
auf den Exit, nicht mehr auf den Einstieg:
Trefferquote 73 % · OeGewinn 13,62 EUR gegen OeVerlust 94,15 EUR (Faktor 7)
Drei von sieben Pending-Trades per `sr_close` bei +0,26 / +2,88 / +0,28 EUR
geschlossen, waehrend die Verlierer bis zum vollen Stop liefen.
Der Squeeze lebt gemessen von LAEUFERN (43 % Treffer, Ertrag im Tail).
⚠⚠ WARUM DIE ALTE MESSUNG NICHT MEHR BINDET. `backtest_squeeze_srclose.py`
(24.07.) fand auf Squeeze-Entries einen WASH (+0,190→+0,224 / +0,394→+0,377) —
aber mit dem **alten** P(break)-Modell und Schwelle **0,55/0,60**. Seit dem
31.07. laeuft das **nachtrainierte** Modell mit Schwelle **0,35**, und das
schliesst live **94-97 %** aller Level-Beruehrungen. Gemessen unter X, betrieben
unter Y — dasselbe Muster wie die acht dokumentierten Deployment-Drift-Faelle.
VORAB FIXIERTE ENTSCHEIDUNGSREGEL:
Der S/R-Close bleibt fuer Squeeze-Trades an, wenn er in BEIDEN Haelften
mindestens so gut ist wie OHNE ihn (Variante A). Sonst: Squeeze-Trades vom
S/R-Close ausnehmen — oder, falls eine Mindestgewinn-Schwelle in BEIDEN
Haelften A schlaegt, diese setzen.
AUFBAU — live-treu, mit den geteilten Kernen:
· Einstiege ueber `core.squeeze_scan.scan(on_entry=...)` = DERSELBE Code wie
der Live-Squeeze. ⚠ `on_entry` statt `exit_fn`, sonst bekommt man still die
Kontrollgruppe ALLER Box-Ausbrueche (real: 519 statt 132).
· Einstieg als ruhende Stop-Order am Level, **Fill bei Beruehrung** — das ist
das Live-Verhalten seit dem 05.08.
· Exit `core.exit_model.simulate` mit dem `stop_when`-Haken; damit wird die
Phasen-Mechanik NICHT kopiert (die Kopiererei hat vier divergierende
Exit-Modelle erzeugt).
· Level ueber `core.sr_levels` (M30-Pivots, geclustert) = derselbe Code wie
`_draw_levels`. P(break) ueber `core.engine._p_break` mit den Live-Gewichten,
Merkmale trainingsgleich aus M5.
· Sequentiell EIN Slot, Echtkosten aus dem Bar-Spread, 2 Halbjahre, Bootstrap-KI.
⚠ ZWEI BEWUSSTE NAEHERUNGEN, ehrlich benannt:
(1) Der Haken schliesst zum BAR-CLOSE; live feuert der Check im 1-s-Takt.
Dieselbe Naeherung wie bei der Flip-Close-Messung.
(2) Live glaettet P(break) ueber ~60 s und hat eine Hysterese von ±5 Pp. Auf
5-Minuten-Bars ist beides gegenstandslos — jeder Bar ist eine frische
Auswertung. Die Schwellen-Naehe wird dadurch etwas haeufiger ueberschritten
als live, also wirkt der S/R-Close hier eher ZU stark. Das faellt zu
Gunsten der Nullhypothese aus (kein Eingriff).
Aufruf: python backtest_squeeze_srclose2.py [bars]
"""
from __future__ import annotations
import random
import sys
from bisect import bisect_left, bisect_right
import MetaTrader5 as mt5
from core.engine import _p_break
from core.exit_model import LIVE, simulate
from core.sr_levels import halten, linien, naechste
from core.squeeze_scan import atr_series, scan
_PIV_K = 3
_LOOK = 50
_ATR5_FLOOR = 0.12
_AT_LEVEL = 0.15 # „am Level" wie live (_sr_close_hint)
_WARTE = 8 # Bars, die die ruhende Order liegt
# Live-Parameter des Squeeze (core/wave_rec.py)
_SQ_N, _SQ_K, _SQ_MULT = 10, 0.1, 2.5
# ⚠⚠ ATR-FLOOR: `core.squeeze_scan` hat als Default ATRMIN_STD = 0,12 — LIVE und
# der dokumentierte Referenzlauf (`backtest_squeeze_touchfill.py`) nutzen aber
# **0,06** (am 2026-07-xx gemessen gesenkt, `backtest_atrfloor.py`: in BEIDEN
# Haelften besser). Der erste Lauf hier nahm den Modul-Default und bekam damit
# eine voellig andere Population — H1 127 gegen H2 483 Einstiege, weil der
# hohe Floor die Niedrig-Vola-Haelfte wegfiltert. Dieselbe Fehlerklasse wie
# das fehlende `set_entry_room(0.6)` am 05.08.: **ein aufgerufenes Gate ist
# nicht dasselbe wie ein aktives Gate.**
_ATRMIN = 0.06
def agg(H, L, C, step):
hh, hl, hc = [], [], []
for s in range(0, len(C) - step + 1, step):
hh.append(max(H[s:s + step])); hl.append(min(L[s:s + step]))
hc.append(C[s + step - 1])
return hh, hl, hc
def pivots_m30(H, L, C):
"""ALLE M30-Pivots EINMAL (Index + Preis) — O(log n) statt quadratisch."""
hh, hl, _ = agg(H, L, C, 6)
ph_i, ph_v, pl_i, pl_v = [], [], [], []
for x in range(_PIV_K, len(hh) - _PIV_K):
if hh[x] == max(hh[x - _PIV_K:x + _PIV_K + 1]):
ph_i.append(x); ph_v.append(round(hh[x], 3))
if hl[x] == min(hl[x - _PIV_K:x + _PIV_K + 1]):
pl_i.append(x); pl_v.append(round(hl[x], 3))
return (ph_i, ph_v), (pl_i, pl_v)
def level_serie(H, L, C, A5):
"""(res, sup) je M5-Bar — DERSELBE Weg wie `_draw_levels`, inkl. Hysterese."""
(ph_i, ph_v), (pl_i, pl_v) = pivots_m30(H, L, C)
out = [(None, None)] * len(C)
pr = ps = None
for i in range(len(C)):
a = A5[i]
if not a:
continue
a5f = max(a, _ATR5_FLOOR)
j = i // 6 - 1
if j < _PIV_K + 5:
continue
a1, b1 = bisect_left(ph_i, j - _LOOK), bisect_right(ph_i, j - _PIV_K)
a2, b2 = bisect_left(pl_i, j - _LOOK), bisect_right(pl_i, j - _PIV_K)
ph, pl = ph_v[a1:b1], pl_v[a2:b2]
if not ph and not pl:
continue
r, s = naechste(linien(ph, pl, a5f), C[i])
r = halten(r, pr, C[i], a5f, True)
s = halten(s, ps, C[i], a5f, False)
pr, ps = r, s
out[i] = (r, s)
return out
def merkmale(C, A5):
"""mom6 / mom3 / ema_diff je Bar — trainingsgleich aus M5."""
def ema(v, p):
k = 2 / (p + 1); o = [v[0]]
for x in v[1:]:
o.append(o[-1] + k * (x - o[-1]))
return o
ef, es = ema(C, 12), ema(C, 50)
m6, m3, ed = [0.0] * len(C), [0.0] * len(C), [0.0] * len(C)
for i in range(6, len(C)):
a5f = max(A5[i] or _ATR5_FLOOR, _ATR5_FLOOR)
m6[i] = (C[i] - C[i - 6]) / a5f
m3[i] = (C[i] - C[i - 3]) / a5f
ed[i] = (ef[i] - es[i]) / a5f
return m6, m3, ed
def entries(H, L, C, A, SP, point, lo, hi):
"""Squeeze-Ausbrueche ueber den GETEILTEN Kern einsammeln."""
got = []
scan(H, L, C, A, SP, point, lo, hi, _SQ_MULT, box_n=_SQ_N, k=_SQ_K,
atr_min=_ATRMIN,
exit_fn=lambda e, d, a, h, l, c, j: 0.0,
on_entry=lambda j, d, lvl, a, komp: (
got.append({"j": j, "d": d, "lvl": lvl, "atr": a}) if komp else None))
return got
def lauf(ein, H, L, C, SP, A5, LV, M6, M3, ED, schwelle, min_gain=0.0):
"""Sequentiell EIN Slot. `schwelle=None` → ohne S/R-Close."""
Rs = []
n_close = 0 # wie oft der S/R-Close wirklich gefeuert hat
frei_ab = -1
grenze = len(C) - LIVE.max_hold - 2
for e in ein:
i, d, lvl = e["j"], e["d"], e["lvl"]
if i < frei_ab or i >= grenze:
continue
a5f = max(A5[i] or _ATR5_FLOOR, _ATR5_FLOOR)
fill = None
for k in range(i + 1, min(i + 1 + _WARTE, grenze)):
if L[k] <= lvl <= H[k]:
fill = k; break
if fill is None:
continue
# ── (1) natuerlicher Exit ohne S/R-Close ─────────────────────────
R, xb = simulate(lvl, d, a5f, H, L, C, fill + 1, LIVE, ret_bar=True)
# ── (2) S/R-Close davor? ─────────────────────────────────────────
# ⚠⚠ NICHT ueber `stop_when`. Der Haken schliesst zum BAR-CLOSE, und
# die Bedingung „am Level" ist ein Fenster von 0,15xATR ≈ 0,018 $ —
# dass ein 5-Minuten-Close genau darin landet, ist selten. Ein
# Probelauf zeigte deshalb Δ ≈ 0,000, waehrend der Close live
# 94-97 % aller Beruehrungen schliesst. Live prueft den LAUFENDEN
# Kurs im Sekundentakt — das Analogon ist die Bar-SPANNE.
# ⚠ Ausstieg zum ERSTEN Moment, in dem die Bedingung feuern kann
# (Level minus Fensterbreite), nicht zum Level selbst. Das ist das
# konservative Ende und entspricht dem 1-s-Loop.
# ⚠ Strikt VOR `xb`: faellt beides in denselben Bar, hat der SL
# Vorrang — dieselbe pessimistische Konvention wie in `simulate`.
if schwelle is not None:
for j in range(fill + 1, xb):
if profit_bedingung(j, d, lvl, a5f, min_gain, LV, C, M6, M3, ED,
H, L, schwelle):
ziel = LV[j][0] if d > 0 else LV[j][1]
R = (ziel - d * _AT_LEVEL * a5f - lvl) * d / a5f
xb = j
n_close += 1
break
Rs.append(R - SP[i] / a5f)
frei_ab = xb + 1
return Rs, n_close
def profit_bedingung(j, d, entry, a5f, min_gain, LV, C, M6, M3, ED, H, L, schwelle):
"""Feuert der S/R-Auto-Close in Bar `j`? — die Live-Bedingungen der Reihe nach."""
ziel = LV[j][0] if d > 0 else LV[j][1] # Ziel-Seite wie _sr_close_hint
if ziel is None:
return False
# „am Level": der Kurs kam im Bar bis auf 0,15xATR heran (Bar-SPANNE, s. o.)
nah = (H[j] >= ziel - _AT_LEVEL * a5f) if d > 0 else (L[j] <= ziel + _AT_LEVEL * a5f)
if not nah:
return False
aus = ziel - d * _AT_LEVEL * a5f # Preis im Moment des Ausloesens
gewinn = (aus - entry) * d / a5f
if gewinn <= 0 or gewinn < min_gain: # live: nur im PLUS
return False
gap = abs(ziel - C[j])
wt = 1.0 if ED[j] * d > 0 else 0.0
return _p_break(M6[j] * d, M3[j] * d, wt, gap / a5f) < schwelle
def ci(Rs, n=3000, seed=42):
if len(Rs) < 20:
return None, None
r = random.Random(seed); m = len(Rs)
b = sorted(sum(Rs[r.randrange(m)] for _ in range(m)) / m for _ in range(n))
return b[int(.025 * n)], b[int(.975 * n)]
def kenn(Rs):
if not Rs:
return None
g = sum(x for x in Rs if x > 0); v = -sum(x for x in Rs if x < 0)
u, o = ci(Rs)
return {"n": len(Rs), "oer": sum(Rs) / len(Rs), "sr": sum(Rs),
"wr": 100 * sum(1 for x in Rs if x > 0) / len(Rs),
"pf": (g / v if v else float("inf")), "u": u, "o": o}
def zeile(name, k, basis=None):
if not k:
return f" {name:<38} —"
kib = " — " if k["u"] is None else f"[{k['u']:+.3f}{k['o']:+.3f}]"
dlt = "" if basis is None else f" Δ {k['oer'] - basis['oer']:+.3f}"
cl = "" if k.get("cl") is None else f" Close {k['cl']:>3} ({k['clq']:>4.1f}%)"
return (f" {name:<38} n={k['n']:>4} WR {k['wr']:>4.1f}% OeR {k['oer']:>+.3f} "
f"PF {k['pf']:>5.2f} SR {k['sr']:>+7.1f} {kib}{dlt}{cl}")
def main():
n = 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)
point = mt5.symbol_info(sym).point
mt5.shutdown()
if bars is None:
print(" keine Bars"); return 1
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]
A5 = atr_series(H, L, C)
LV = level_serie(H, L, C, A5)
M6, M3, ED = merkmale(C, A5)
mid = len(C) // 2
e1 = entries(H, L, C, A5, SP, point, 60, mid)
e2 = entries(H, L, C, A5, SP, point, mid, len(C))
print(f"\n {sym} · {len(C)} M5-Bars · Squeeze-Entries H1 {len(e1)} · H2 {len(e2)}")
print(f" Einstieg ruhend am Level (Fill bei Beruehrung) · kanonischer Exit · "
f"Echtkosten · EIN Slot")
print(f" Regel vorab: der S/R-Close bleibt an, wenn er in BEIDEN Haelften "
f"mindestens so gut ist wie OHNE ihn.\n")
def rechne(schw, mg=0.0):
r1, c1 = lauf(e1, H, L, C, SP, A5, LV, M6, M3, ED, schw, mg)
r2, c2 = lauf(e2, H, L, C, SP, A5, LV, M6, M3, ED, schw, mg)
k1, k2 = kenn(r1), kenn(r2)
for k, c in ((k1, c1), (k2, c2)):
if k:
k["cl"] = c
k["clq"] = 100 * c / k["n"] if k["n"] else 0
return k1, k2
b1, b2 = rechne(None)
print(" ── BASIS: OHNE S/R-Close (nur kanonischer Exit) ───────────────")
print(zeile("A) ohne S/R-Close H1", b1))
print(zeile("A) ohne S/R-Close H2", b2))
print("\n ── MIT S/R-Close, Schwellen-Sweep (live = 0,35) ───────────────")
for s in (0.30, 0.35, 0.45, 0.55):
k1, k2 = rechne(s)
mark = " ← LIVE" if abs(s - 0.35) < 1e-9 else ""
print(zeile(f"B) p_break < {s:.2f} H1{mark}", k1, b1))
print(zeile(f"B) p_break < {s:.2f} H2{mark}", k2, b2))
print("\n ── ABHILFE-VARIANTE: Close erst ab Mindestgewinn (bei 0,35) ───")
for mg in (0.5, 1.0, 1.5):
k1, k2 = rechne(0.35, mg)
print(zeile(f"C) 0,35 + erst ab {mg:.1f}xATR H1", k1, b1))
print(zeile(f"C) 0,35 + erst ab {mg:.1f}xATR H2", k2, b2))
l1, l2 = rechne(0.35)
print("\n ── URTEIL ─────────────────────────────────────────────────────")
ok = bool(l1 and l2 and b1 and b2
and l1["oer"] >= b1["oer"] and l2["oer"] >= b2["oer"])
print(f" S/R-Close (0,35) mindestens so gut wie ohne, in BEIDEN: "
f"{'✅ ja — anlassen' if ok else '❌ nein'}")
if not ok:
print(f" H1 {l1['oer']:+.3f} gegen {b1['oer']:+.3f} "
f"H2 {l2['oer']:+.3f} gegen {b2['oer']:+.3f}")
print(" ➜ Squeeze-Trades vom S/R-Close ausnehmen, ODER eine "
"Mindestgewinn-Schwelle, die BEIDE Haelften schlaegt (Tabelle C).")
return 0
if __name__ == "__main__":
raise SystemExit(main())