(2) conf_pct trennt im Markt-Modus NICHT: der Bandverlauf ist nicht gleichgerichtet (H1 bestes Band 0-44, schlechtestes 55-64), und die Schwelle bewegt nichts -- >=0 liefert H1 -0,189, >=55 liefert -0,197. 11,4 % Signalmenge fuer 0,017 R. Mit ruhender Order trennt die obere Haelfte: >=75 schlaegt die Live-Schwelle im OeR beidhaelftig (-0,001/+0,147 gegen -0,070/+0,081), aber die Summe faellt (+82,8 gegen +146,1). Regel verlangte beides -> nicht geaendert, Regel NICHT nachtraeglich gelockert. 27. verworfener Signal-Eingriff. Dritte unabhaengige Bestaetigung des Ausfuehrungs-Hebels am selben Tag: Level gegen Markt +0,127/+0,096 R, groessengleich zu backtest_entryroom_level. (3) M15-Karte steht 37,8 % der ZEIT gruen (in Wechseln gerechnet waeren es 49 % -- gruene Phasen sind kuerzer). Handlungsrelevanter Zustand entkoppelt 54,1 % / 51,5 %, beide KI enthalten die 50 %. 205 von 350 unabhaengigen Faellen, also in gut zwei Wochen entscheidbar. Die Hysterese vom 12.08. wirkt (53 -> 54,1 %). Gate 3b ins Banner zu ziehen ist durch den Entry-Raum-Lauf schon beantwortet: diese Dimension trennt nicht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
275 lines
11 KiB
Python
275 lines
11 KiB
Python
"""Traegt der ENTRY-RAUM-Filter noch, wenn man am Level einsteigt? (2026-08-19)
|
|
|
|
FRAGE (User: "hast Du Ideen wie ich die Empfehlung noch verbessern koennte?").
|
|
`entry_room` ist mit 60,6 % der mit ABSTAND groesste Blocker der Empfehlung
|
|
(gerichtet sind nur 2,7 %). Kalibriert wurde er am 2026-07-16 mit
|
|
`backtest_entryroom.py` -- dort steigt die Sim am BAR-CLOSE ein, also per MARKT.
|
|
Die Begruendung lautet folgerichtig: Gegenlevel nah => Ertrag gedeckelt =>
|
|
nicht handeln (PF 0,42/0,66, sauber gemessen).
|
|
|
|
Am 05./06.08. hat sich die Ausfuehrung geaendert und das Gate wurde NICHT
|
|
nachgezogen. Seither ist belegt, dass eine RUHENDE ORDER AM LEVEL etwas
|
|
grundlegend anderes ist als ein Markt-Einstieg mit dem Level als Hindernis
|
|
voraus (Squeeze: -0,158 => +0,124/+0,292).
|
|
|
|
Markt-Einstieg unter einem Widerstand = in die Wand hineinkaufen
|
|
BUY_STOP am Widerstand = Ausbruch aus der Wand heraus
|
|
|
|
Das sind GEGENTEILIGE Trades, und das Gate kann sie nicht unterscheiden --
|
|
es blockt beide.
|
|
|
|
GEMESSEN wird deshalb die von `entry_room` GEBLOCKTE Gruppe getrennt, genau
|
|
wie `backtest_htf_vola.py` es fuer den M30-Filter gemacht hat:
|
|
A) Markt am Bar-Close (was die Sim von 2026-07-16 unterstellt)
|
|
B) ruhende Stop-Order AM blockierenden Level, Fill bei BERUEHRUNG
|
|
|
|
Das Level ist ein M5-Pivot aus der Vergangenheit, es STEHT also -- das ist der
|
|
Unterschied zu `_pend` (wandert, `backtest_signal_touchfill.py` faellt durch)
|
|
und dieselbe Eigenschaft wie die Squeeze-Box (steht, haelt).
|
|
|
|
REGEL VORAB FIXIERT -- gebaut wird nur, wenn ALLES zutrifft:
|
|
(1) geblockte Gruppe mit Level-Einstieg: OeR > 0 UND PF > 1 in BEIDEN Haelften
|
|
(2) 95-%-KI schliesst in BEIDEN Haelften die Null aus
|
|
(3) die Nachbarn (Puffer, Gueltigkeitsdauer) kippen nicht
|
|
(4) n >= 100 je Haelfte
|
|
|
|
GRENZEN, vorab benannt:
|
|
- Kein sequentieller EIN-Slot-Lauf. Verglichen werden zwei POPULATIONEN
|
|
(geblockt/durchgelassen), nicht zwei Kontostaende; dafuer ist der OeR je
|
|
Trade mit KI die richtige Statistik. Ein Slot-Lauf braeuchte eine
|
|
Vorrang-Entscheidung und misst dann Selektion statt Gate.
|
|
- Exit-Slippage ist NICHT modelliert (senkt alle Varianten, die haeufiger
|
|
handelnden staerker).
|
|
- Der ATR fuer die R-Normierung ist in A und B derselbe (Signal-Bar), sonst
|
|
waeren die R-Einheiten nicht vergleichbar.
|
|
|
|
Aufruf: python backtest_entryroom_level.py [bars]
|
|
"""
|
|
import sys
|
|
from bisect import bisect_left, bisect_right
|
|
|
|
import MetaTrader5 as mt5
|
|
|
|
from backtest_breakout_gated import (_ATRMIN, _ATR_FLOOR_PB, _COOL, _PIV_K,
|
|
_PIV_LOOK, _TIMEOUT_BARS, _TU,
|
|
_atr_series, _htf_sign)
|
|
from backtest_cost_gate import kennzahlen
|
|
from core.analysis import calc_trend_angle
|
|
from core.exit_model import LIVE, simulate
|
|
from core.wave_rec import (WaveRecommender, _ANGLE_LR, _EMA_FAST, _EMA_SLOW,
|
|
_ema_series)
|
|
|
|
_K = 0.3 # breakout_k, Live-Wert
|
|
_GATE = 0.6 # entry_room_atr, Live-Wert
|
|
|
|
|
|
def sammle(w, H, L, C, A, EF, ES, AD, HTF, H1, PH, PL, lo, hi):
|
|
"""Alle Signale NACH _build + _confirm_breakout, VOR dem Raum-Gate.
|
|
|
|
Geliefert wird je Signal die Rohlage; das Gate wird hier bewusst NICHT
|
|
angewandt -- seine Entscheidung ist der Gegenstand der Messung.
|
|
"""
|
|
out = []
|
|
pend = None
|
|
i = max(lo, _PIV_LOOK + _EMA_SLOW + 5)
|
|
while i < hi:
|
|
atr = A[i]
|
|
if not atr or atr < _ATRMIN:
|
|
i += 1
|
|
continue
|
|
w._pb_feats = {"atr": max(atr, _ATR_FLOOR_PB)}
|
|
_a1 = bisect_left(PH, i - _PIV_LOOK); _b1 = bisect_right(PH, i - _PIV_K)
|
|
_a2 = bisect_left(PL, i - _PIV_LOOK); _b2 = bisect_right(PL, i - _PIV_K)
|
|
w._pb_levels = {"ph": [C[j] for j in PH[_a1:_b1]],
|
|
"pl": [C[j] for j in PL[_a2:_b2]]}
|
|
rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5,
|
|
htf_trend=HTF[i], h1_trend=H1[i], angle=AD[i])
|
|
sig = rec.get("signal")
|
|
|
|
if _K > 0 and sig in ("LONG", "SHORT"):
|
|
d = 1 if sig == "LONG" else -1
|
|
if pend is None or pend["dir"] != d:
|
|
pend = {"dir": d, "level": C[i] + d * _K * atr,
|
|
"invalid": C[i] - d * _K * atr, "confirmed": False, "bar": i}
|
|
if not pend["confirmed"]:
|
|
broke = (C[i] >= pend["level"]) if d > 0 else (C[i] <= pend["level"])
|
|
gegen = (C[i] <= pend["invalid"]) if d > 0 else (C[i] >= pend["invalid"])
|
|
if broke:
|
|
pend["confirmed"] = True
|
|
else:
|
|
if gegen or (i - pend["bar"]) > _TIMEOUT_BARS:
|
|
pend = {"dir": d, "level": C[i] + d * _K * atr,
|
|
"invalid": C[i] - d * _K * atr,
|
|
"confirmed": False, "bar": i}
|
|
i += 1
|
|
continue
|
|
elif sig == "WARTEN":
|
|
pend = None
|
|
|
|
if sig not in ("LONG", "SHORT"):
|
|
i += 1
|
|
continue
|
|
|
|
d = 1 if sig == "LONG" else -1
|
|
# ECHTE Methode, nicht nachgebaut -- dieselbe Zahl, die das Gate benutzt.
|
|
ri = w.room_info(C[i])[("long" if d > 0 else "short")] or {}
|
|
out.append({"i": i, "d": d, "atr": max(atr, _ATRMIN), "px": C[i],
|
|
"conf": rec.get("conf_pct"), # additiv, s. backtest_conf_gated.py
|
|
"lvl": ri.get("level"), "dist": ri.get("dist_atr"),
|
|
"frei": bool(ri.get("frei"))})
|
|
pend = None
|
|
i += _COOL
|
|
return out
|
|
|
|
|
|
def handeln(sig, H, L, C, SP, ende, modus, puffer, gueltig):
|
|
"""R eines Signals. modus 'markt' = Bar-Close, 'level' = ruhende Stop-Order."""
|
|
i, d, atr = sig["i"], sig["d"], sig["atr"]
|
|
|
|
def cost(j):
|
|
return (SP[j] if SP[j] > 0 else 0.0225) / atr
|
|
|
|
if modus == "markt":
|
|
entry, j = C[i], i
|
|
else:
|
|
lvl = sig["lvl"]
|
|
if lvl is None:
|
|
return None # freie Bahn: kein Level zum Liegen
|
|
entry = lvl + d * puffer * atr
|
|
# Stop-Order MUSS auf der richtigen Marktseite liegen (Lehre 06.08.:
|
|
# ohne diese Pruefung fuellt die Sim zu laengst passierten Preisen).
|
|
if (d > 0 and entry <= C[i]) or (d < 0 and entry >= C[i]):
|
|
return None
|
|
j = None
|
|
for x in range(i + 1, min(i + 1 + gueltig, ende)):
|
|
if (H[x] >= entry) if d > 0 else (L[x] <= entry):
|
|
j = x
|
|
break
|
|
if j is None:
|
|
return None # nicht gefuellt = kein Trade
|
|
if j + 1 >= ende:
|
|
return None
|
|
r = simulate(entry, d, atr, H, L, C, j + 1, LIVE, timestop=True, use_tp=True)
|
|
return r - cost(j)
|
|
|
|
|
|
def gruppen(sigs, H, L, C, SP, ende, puffer, gueltig):
|
|
g = {"blk_markt": [], "blk_level": [], "frei_markt": [],
|
|
"durch_markt": [], "durch_level": []}
|
|
for s in sigs:
|
|
rm = handeln(s, H, L, C, SP, ende, "markt", puffer, gueltig)
|
|
rl = handeln(s, H, L, C, SP, ende, "level", puffer, gueltig)
|
|
if s["frei"] or s["lvl"] is None:
|
|
if rm is not None:
|
|
g["frei_markt"].append(rm)
|
|
continue
|
|
blk = (s["dist"] is not None and s["dist"] < _GATE)
|
|
if rm is not None:
|
|
g["blk_markt" if blk else "durch_markt"].append(rm)
|
|
if rl is not None:
|
|
g["blk_level" if blk else "durch_level"].append(rl)
|
|
return g
|
|
|
|
|
|
def zeig(lbl, k1, k2):
|
|
def f(k):
|
|
if not k:
|
|
return " --"
|
|
ci = k.get("ci")
|
|
m = ""
|
|
if ci:
|
|
m = (" [{:+.3f} .. {:+.3f}]".format(ci[0], ci[1])
|
|
+ ("" if ci[0] * ci[1] > 0 else " ~KI enthaelt 0"))
|
|
return ("n={:>4} WR={:>3.0f}% OeR={:+.3f} PF={:.2f}{}"
|
|
.format(k["n"], k["wr"], k["oer"], k["pf"], m))
|
|
print(" {:<28} H1 {}".format(lbl, f(k1)))
|
|
print(" {:<28} H2 {}".format("", f(k2)))
|
|
|
|
|
|
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()
|
|
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)
|
|
EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW)
|
|
N = len(C); mid = N // 2; ende = N - LIVE.max_hold - 1
|
|
|
|
print("Vorberechnung (Winkel, HTF-Trends, Pivots) ...", flush=True)
|
|
AD = [90.0] * N
|
|
for i in range(_ANGLE_LR + 2, N):
|
|
AD[i] = calc_trend_angle(C[i - (_ANGLE_LR + 1):i + 1], _ANGLE_LR)
|
|
HTF = [0] * N; H1 = [0] * N
|
|
for i in range(N):
|
|
if A[i]:
|
|
HTF[i] = _htf_sign(C, i, 6, A[i])
|
|
H1[i] = _htf_sign(C, i, 12, A[i])
|
|
PH = [j for j in range(_PIV_K, N - _PIV_K)
|
|
if H[j] == max(H[j - _PIV_K:j + _PIV_K + 1])]
|
|
PL = [j for j in range(_PIV_K, N - _PIV_K)
|
|
if L[j] == min(L[j - _PIV_K:j + _PIV_K + 1])]
|
|
|
|
w = WaveRecommender(_TU(), mt5.TIMEFRAME_M5)
|
|
w.set_reversal_enabled(False)
|
|
w.set_entry_room(_GATE) # Live-Konfiguration, nicht Default
|
|
w.set_dead_hours([])
|
|
w.set_breakout_k(_K)
|
|
|
|
print("=" * 96)
|
|
print(" ENTRY-RAUM-GATE gegen AUSFUEHRUNG -- {} M5 ({} Bars, 2 Halbjahre)"
|
|
.format(sym, N))
|
|
print(" Gate {}x ATR_M5 - breakout_k {} - Reversal AUS - Exit kanonisch"
|
|
.format(_GATE, _K))
|
|
print("=" * 96)
|
|
|
|
print("Signale sammeln (voller Gate-Stack, VOR dem Raum-Gate) ...", flush=True)
|
|
s1 = sammle(w, H, L, C, A, EF, ES, AD, HTF, H1, PH, PL, 0, mid)
|
|
s2 = sammle(w, H, L, C, A, EF, ES, AD, HTF, H1, PH, PL, mid, ende)
|
|
print(" H1 {} - H2 {} Signale vor dem Raum-Gate".format(len(s1), len(s2)))
|
|
|
|
PU, GU = 0.0, 12
|
|
g1 = gruppen(s1, H, L, C, SP, ende, PU, GU)
|
|
g2 = gruppen(s2, H, L, C, SP, ende, PU, GU)
|
|
|
|
print("")
|
|
print(" HAUPTLAUF (Puffer {}x ATR - Order gueltig {} Bars = 1 h)".format(PU, GU))
|
|
print(" " + "-" * 92)
|
|
for key, lbl in (("blk_markt", "GEBLOCKT - Markt"),
|
|
("blk_level", "GEBLOCKT - Level (ruhend)"),
|
|
("durch_markt", "durchgel. - Markt (=heute)"),
|
|
("durch_level", "durchgel. - Level (ruhend)"),
|
|
("frei_markt", "freie Bahn - Markt")):
|
|
zeig(lbl, kennzahlen(g1[key]), kennzahlen(g2[key]))
|
|
print("")
|
|
|
|
print(" NACHBARN (Regel 3) -- nur die geblockte Gruppe am Level")
|
|
print(" " + "-" * 92)
|
|
for pu, gu in ((0.05, 12), (0.10, 12), (0.0, 6), (0.0, 24)):
|
|
a = gruppen(s1, H, L, C, SP, ende, pu, gu)["blk_level"]
|
|
b = gruppen(s2, H, L, C, SP, ende, pu, gu)["blk_level"]
|
|
zeig("Puffer {:.2f} - gueltig {}".format(pu, gu),
|
|
kennzahlen(a), kennzahlen(b))
|
|
print("")
|
|
|
|
k1, k2 = kennzahlen(g1["blk_level"]), kennzahlen(g2["blk_level"])
|
|
ok = bool(k1 and k2 and k1["n"] >= 100 and k2["n"] >= 100
|
|
and k1["oer"] > 0 and k2["oer"] > 0
|
|
and k1["pf"] > 1 and k2["pf"] > 1
|
|
and k1["ci"] and k2["ci"]
|
|
and k1["ci"][0] > 0 and k2["ci"][0] > 0)
|
|
print("=" * 96)
|
|
print(" URTEIL nach der vorab fixierten Regel: {}"
|
|
.format("ERFUELLT -- weiterverfolgen" if ok
|
|
else "NICHT erfuellt -- nichts bauen"))
|
|
print("=" * 96)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|