Files
AH-Oil-Trader/backtest_squeeze_exit3.py
T
Axel HocksandClaude Opus 5 d8a243612a Gegen-Squeeze als Exit zum 3. Mal gemessen: der Mechanismus feuert praktisch nie
Anlass: Trade T=49423144 (-343 EUR) und der Wunsch, der Breakout haette frueher
schliessen sollen - nachgeschaerft zu "bei einem Squeeze den SL sofort eng
setzen". Beide Varianten gemessen, kanonischer Exit + stop_when-Hook (keine
Exit-Kopie), Echtkosten, 2 Halbjahre, zwei Populationen.

Der eigentliche Befund steht nicht in den Kennzahlen, sondern in den
Ausloesungen: sofort-schliessen feuert 1x von 809 bzw. 1274 Squeeze-Trades, der
enge SL 0x. Bei Wellen-Entries 8x bzw. 2-3x. Grund ist konstruktionsbedingt: ein
Gegen-Ausbruch verlangt eine NEUE Kompression (Box ueber 10 M5-Bars = 50 min),
waehrend die mittlere Haltedauer bei 12-18 min liegt. Der Gegen-Ausbruch trifft
also in ~0,1 % (Squeeze) bzw. ~1 % (Welle) der Trades ein - der Fall des Users
war einer davon.

Damit sind die Kennzahl-Unterschiede Rauschen aus einer Handvoll Faelle, und
keine Variante besteht auf beiden Populationen. Die abgeschwaechte Form (enger
SL statt Close) adressiert zwar genau den alten Einwand (Gegen-Ausbrueche sind
oft Fehlausbrueche), kann ihn aber nicht ausspielen, wenn sie 0-mal ausloest -
und waere von einer Bar-Simulation ohnehin ueberschaetzt.

Dritte Ablehnung, diesmal mit einer Begruendung, die nicht vom Exit-Modell
abhaengt.

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

219 lines
9.4 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
"""Gegen-Squeeze als EXIT — dritte Messung, jetzt mit dem kanonischen Exit.
ANLASS (User, 2026-08-05): Trade T=49423144 (SELL 3,49 @ 75,012) lief in 14 min
1,135 $ ins Minus und endete am Initial-SL bei **343 €**. Wunsch: „der Breakout
hätte den Trade viel früher schließen sollen."
⚠ WAS AN DEM TAG WIRKLICH PASSIERT IST (rekonstruiert):
* Beim Einstieg war der Squeeze **active SHORT** — er stand also FÜR den Trade,
nicht dagegen. Die Empfehlung war WARTEN (`block=entry_room`), der Einstieg
manuell.
* Der Gegen-Ausbruch (armed LONG @ 75,729 auf dem 17:05-Bar) wurde erst
~35 min SPÄTER aktiv. Ein Close dort hätte bei ≈75,73 stattgefunden:
(75,73 75,012) × 3,49 Lots × 87,6 ≈ **219 €** statt 343 € → ~124 € gespart.
Das ist EIN Fall. Die Idee ist im Projekt bereits ZWEIMAL gemessen und verworfen
(`backtest_squeeze_exit.py` 22.07., `backtest_squeeze_exit_vol.py` 24.07.).
WARUM TROTZDEM EINE DRITTE MESSUNG: beide Vorläufer stammen von VOR der
Exit-Vereinheitlichung am 31.07. und ruhen damit auf einem Exit-Modell, das dem
Live-System nicht entspricht (Trail 1,5 statt 1,0, teils ohne Time-Stop). Genau
diese Konstellation hat heute schon mehrere alte Schlüsse gekippt.
MESSUNG: kanonischer Exit (`exit_model.simulate`) + der dokumentierte
**`stop_when`-Hook** — dadurch muss die Phasen-Mechanik NICHT kopiert werden
(genau diese Kopiererei hat im Projekt vier divergierende Exit-Modelle erzeugt).
Zwei Populationen (Squeeze-Ausbrüche, gegatete Wellensignale), feste Entry-Listen,
Echtkosten, 2 Halbjahre.
VORAB FIXIERTE ENTSCHEIDUNGSREGEL:
Einbauen nur, wenn eine Variante ΣR **und** ØR in BEIDEN Halbjahren und auf
BEIDEN Populationen verbessert. Sonst bleibt es beim Nein — dann zum dritten Mal.
Aufruf: python backtest_squeeze_exit3.py [bars]
"""
import sys
import MetaTrader5 as mt5
import core.wave_rec as wr
from backtest_cost_gate import _atr_series, kandidaten
from backtest_trail_be import _ema_series, _htf_sign, wellen_entries
from core.analysis import calc_trend_angle
from core.exit_model import LIVE, simulate
from core.wave_rec import (WaveRecommender, _EMA_FAST, _EMA_SLOW, _N_BARS,
_ANGLE_LR, _ANGLE_DEAD)
_ATRMIN = 0.06
_PIV_K, _PIV_LOOK = 3, 300
def squeeze_richtung(H, L, C, A):
"""Je Bar: +1 wenn ein LONG-Ausbruch aktiv ist, 1 bei SHORT, sonst 0.
Nutzt die ECHTE `_squeeze_one` — kein Nachbau."""
out = [0] * len(C)
for i in range(wr._SQ_N + 2, len(C)):
a = A[i]
if not a or a < _ATRMIN:
continue
w0 = i - wr._SQ_N - 2
st = WaveRecommender._squeeze_one(H[w0:i + 1], L[w0:i + 1], C[w0:i + 1], a)
if st.get("state") == "active":
out[i] = 1 if st.get("dir") == "LONG" else -1
return out
def lauf(K, H, L, C, SQ, lo, hi, modus, min_verlust=0.0):
"""modus: 'aus' | 'immer' | 'nur_minus' | 'eng_sl'.
⚠ `eng_sl` ist die User-Idee vom 05.08.: beim Gegen-Ausbruch NICHT sofort
schliessen, sondern den Stop ENG ans aktuelle Niveau ziehen. Der Unterschied
ist inhaltlich: die Sofort-Variante fiel gemessen durch, WEIL Gegen-Ausbrueche
oft Fehlausbrueche sind und die Position zurueckprallt. Ein enger Stop steigt
nur aus, wenn die Bewegung WEITERLAEUFT — genau der Einwand.
Modelliert ueber den Hook: Preis beim ersten Gegen-Ausbruch merken, schliessen
sobald es `min_verlust` xATR WEITER dagegen geht.
⚠ Bar-Aufloesung: ein enger Stop wird von dieser Simulation systematisch
UEBERSCHAETZT (s. analyze_trail_resolution.py, 05.08.) — die Zahlen unten sind
also die optimistische Obergrenze."""
rs = []
frei = lo
ausgeloest = 0
for k in K:
if not (lo <= k["i"] < hi) or k["i"] < frei:
continue
d = k["d"]
traf = []
ausl = [] # Profit-Stand beim ERSTEN Gegen-Ausbruch
def stop_when(j, profit, _d=d, _t=traf, _a=ausl):
if modus == "aus":
return False
if modus == "eng_sl":
if not _a:
if SQ[j] != -_d:
return False
_a.append(profit) # Stop ab hier eng nachziehen
return False
if profit <= _a[0] - min_verlust:
_t.append(j); return True
return False
if SQ[j] != -_d: # kein GEGEN-Ausbruch
return False
if modus == "nur_minus" and profit > -min_verlust:
return False
_t.append(j)
return True
r, xb = simulate(k["lvl"], d, k["atr"], H, L, C, k["i"] + 1, LIVE,
timestop=True, use_tp=True,
stop_when=(None if modus == "aus" else stop_when),
ret_bar=True)
rs.append(r - k["kosten"])
ausgeloest += 1 if traf else 0
frei = xb + 1
return rs, ausgeloest
def kenn(rs):
if not rs:
return None
n = len(rs); s = sum(rs)
g = sum(x for x in rs if x > 0); v = -sum(x for x in rs if x < 0)
return {"n": n, "wr": 100 * sum(1 for x in rs if x > 0) / n, "oer": s / n,
"sr": s, "pf": (g / v) if v > 0 else float("inf"), "worst": min(rs)}
def zeile(lbl, k1, k2, b1, b2):
def f(k):
if not k:
return f"{'—':>36}"
return (f"{k['n']:>5} {k['wr']:>5.1f}% {k['oer']:>+7.3f} "
f"{k['pf']:>5.2f} {k['sr']:>+8.1f}")
gut = bool(k1 and k2 and b1 and b2 and k1["oer"] > b1["oer"]
and k2["oer"] > b2["oer"] and k1["sr"] > b1["sr"] and k2["sr"] > b2["sr"])
print(f" {lbl:<26}{f(k1)} |{f(k2)}" + (" BESSER" if gut else ""))
return gut
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)
EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW)
AD = [0.0] * len(C)
for i in range(_N_BARS, len(C)):
AD[i] = calc_trend_angle(C[i - _N_BARS:i + 1], _ANGLE_LR) - 90.0
if abs(AD[i]) < _ANGLE_DEAD:
AD[i] = 0.0
HTF = [_htf_sign(C, i, 6, A[i] or 0.1) for i in range(len(C))]
H1S = [_htf_sign(C, i, 12, A[i] or 0.1) for i in range(len(C))]
PH = [j for j in range(_PIV_K, len(C) - _PIV_K)
if H[j] == max(H[j - _PIV_K:j + _PIV_K + 1])]
PL = [j for j in range(_PIV_K, len(C) - _PIV_K)
if L[j] == min(L[j - _PIV_K:j + _PIV_K + 1])]
w = WaveRecommender(lambda: None)
w.set_reversal_enabled(False)
w.set_entry_room(0.6)
w.set_dead_hours("")
mid = len(C) // 2
print("=" * 100)
print(f" GEGEN-SQUEEZE ALS EXIT — 3. Messung · {sym} M5, {len(C)} Bars")
print(f" kanonischer Exit (Trail {LIVE.mult}) + stop_when-Hook · Echtkosten · 2 Halbjahre")
print(" ⚠ Idee bereits 2x verworfen (22.07. / 24.07.) — beide auf einem NICHT-Live-Exit.")
print("=" * 100)
SQ = squeeze_richtung(H, L, C, A)
print(f" Bars mit aktivem Ausbruch: {sum(1 for x in SQ if x)} von {len(SQ)}")
pops = [("SQUEEZE-Entries", kandidaten(H, L, C, T, SP, A)),
("WELLE (gegated)",
wellen_entries(w, H, L, C, A, SP, EF, ES, AD, HTF, H1S, PH, PL))]
treffer = {}
for name, K in pops:
print(f"\n {name}{len(K)} Entries")
print(f" {'Variante':<26}{'H1 n WR OeR PF SigmaR':>36}"
f" |{'H2 n WR OeR PF SigmaR':>36}")
b1 = kenn(lauf(K, H, L, C, SQ, 0, mid, "aus")[0])
b2 = kenn(lauf(K, H, L, C, SQ, mid, len(C), "aus")[0])
zeile("BASIS (kein Gegen-Exit)", b1, b2, None, None)
for modus, mv, lbl in (("immer", 0.0, "sofort schliessen"),
("nur_minus", 0.0, "sofort, nur im Minus"),
("nur_minus", 0.5, "sofort, Verlust > 0,5xATR"),
("eng_sl", 0.1, "ENGER SL 0,1xATR"),
("eng_sl", 0.2, "ENGER SL 0,2xATR"),
("eng_sl", 0.3, "ENGER SL 0,3xATR"),
("eng_sl", 0.5, "ENGER SL 0,5xATR")):
r1, a1 = lauf(K, H, L, C, SQ, 0, mid, modus, mv)
r2, a2 = lauf(K, H, L, C, SQ, mid, len(C), modus, mv)
k1, k2 = kenn(r1), kenn(r2)
if zeile(lbl + f" [{a1}/{a2}x]", k1, k2, b1, b2):
treffer.setdefault(lbl, set()).add(name)
print("\n" + "=" * 100)
print(" URTEIL (Regel VOR dem Lauf fixiert: OeR UND SigmaR besser, in BEIDEN")
print(" Haelften UND auf BEIDEN Populationen)")
print("=" * 100)
voll = [k for k, v in treffer.items() if len(v) == len(pops)]
if voll:
print(f" => BESTEHT: {voll}")
else:
print(" => KEINE Variante besteht auf beiden Populationen.")
print(" Der Gegen-Squeeze taugt als ENTRY (validiert), nicht als EXIT —")
print(" zum DRITTEN Mal, jetzt auch mit dem kanonischen Exit.")
print()
if __name__ == "__main__":
main()