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>
316 lines
14 KiB
Python
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())
|