backtest_cost_gate.ci95(): 95-%-Bootstrap-KI des Mittelwerts (Perzentil, 2000 Resamples, random_state fest -> reproduzierbar, ab n>=20). kennzahlen() liefert es als neuen Schluessel `ci` — rein additiv, alle 11 Aufrufer lesen weiter nur n/wr/oer/sr/pf, keine dokumentierte Zahl verschiebt sich (geprueft). Sofortiger Ertrag: der erste Lauf relativiert das `3 von 7 tragen` vom 06.08. Auf der entscheidenden Spalte C (Beruehrungs-Fill): KONTROLLE Squeeze H1 [+0,037…+0,229] H2 [+0,225…+0,366] PDH / PDL H1 [-0,032…+0,544]⚠ H2 [+0,155…+0,764] Equal H/L H1 [+0,001…+0,425] H2 [-0,035…+0,356]⚠ Asian Range H1 [-0,067…+0,357]⚠ H2 [-0,060…+0,332]⚠ Nur der Squeeze hat in BEIDEN Haelften ein Intervall klar ueber null. Bei Asian Range enthalten beide die Null — als Ueberlebender war das zu stark formuliert. Formulierung auf `1 gesichert, 2 unklar` korrigiert. Zwei verschiedene Fragen, beide noetig: die 2-Stichproben-Regel prueft die Stabilitaet des VORZEICHENS ueber Regime, das KI die Unterscheidbarkeit vom RAUSCHEN innerhalb einer Stichprobe. Hier war Ersteres erfuellt, Letzteres nicht. Ein KI, das die Null enthaelt, heisst nicht widerlegt, sondern nicht belegt — und zwei gleichgerichtete Haelften sind zusammen mehr Evidenz als jede fuer sich. Grenze dokumentiert: der Bootstrap unterstellt unabhaengige Ziehungen. Fuer die sequentielle Sim mit EINEM Slot passt das; fuer ueberlappende Fenster (analyze_hl_funding.py) ist er zu optimistisch — dort braeuchte es einen Block-Bootstrap. ci95 ist dafuer ausdruecklich NICHT gedacht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
295 lines
12 KiB
Python
295 lines
12 KiB
Python
#!/usr/bin/env python3
|
||
"""Die verworfenen Signale — neu gemessen auf der EINEN Achse, die heute neu ist.
|
||
|
||
ANLASS (2026-08-06, User: „backteste alle verworfenen Signale").
|
||
|
||
⚠ WAS DAS HIER NICHT IST: ein erneuter Durchlauf aller Ablehnungen. Der lief am
|
||
2026-08-04 (`run_all_backtests.py`, 14 Klassen, alle blieben verworfen) und waere
|
||
nur eine Wiederholung — mit dem Mehrfachvergleichs-Problem obendrauf.
|
||
|
||
NEU ist genau eine Achse: der **Einstiegspreis**. Sie hat am 05.08. Squeeze und
|
||
Auto-Signal von negativ auf positiv gedreht. ALLE alten Ablehnungen wurden mit
|
||
Markt-/Bar-Close-Einstieg gemessen — sie koennten also unter einer Annahme
|
||
gefallen sein, die nicht mehr gilt.
|
||
|
||
⚠⚠ ABER MIT DER HEUTIGEN KORREKTUR. `backtest_momentum_v2.py` hat gezeigt, dass
|
||
„Einstieg am Level" zweideutig ist:
|
||
(B) Fill, WO der Bar-CLOSE das Level bricht → Look-ahead, ~+0,3…0,45 R zu gut
|
||
(C) Fill bei der ERSTEN BERUEHRUNG → das, was live passiert
|
||
Gemessen wird deshalb C. B laeuft nur als Diagnose mit, um den Aufschlag sichtbar
|
||
zu machen.
|
||
|
||
⚠⚠ UND NUR DORT, WO DIE ACHSE UEBERHAUPT GREIFEN KANN. Der Vorteil entsteht an
|
||
Levels, die **STILLSTEHEN** (Squeeze-Box: +0,124/+0,292) — nicht an mitwandernden
|
||
(Momentum: −0,04…−0,09). Geprueft werden deshalb die verworfenen Setups mit einem
|
||
stehenden Level und einer AUSBRUCHS-Richtung:
|
||
ORB (EU/US) · PDH/PDL · Asian Range · Equal Highs/Lows
|
||
⚠ Bewusst NICHT dabei, weil die Achse dort nichts aendern kann:
|
||
· Momentum, Inter-Market, Chop-Gate, Meta-Labeling, RSI/MACD/ADX — kein Level.
|
||
· Reversal/Bounce, Kerzen-Fade, POC-Reversion, Fibonacci, Kanalrand — REVERSIONS-
|
||
Einstiege. Dort fuellt eine ruhende Order gerade dann, wenn man falsch liegt
|
||
(gemessen `backtest_candle_fade2.py`: Limit am Extrem macht es SCHLECHTER).
|
||
· Volume Profile — Level steht zwar, aber das Profil muesste je Bar neu gebinnt
|
||
werden; Laufzeit unverhaeltnismaessig. Offen.
|
||
|
||
VORAB FIXIERTE ENTSCHEIDUNGSREGEL (vor dem ersten Lauf):
|
||
(1) in Variante C: ØR > 0 UND PF > 1 in BEIDEN Haelften
|
||
(2) mind. 40 Trades je Haelfte
|
||
(3) muss die KONTROLLE (Squeeze, +0,124/+0,292) nicht schlagen, aber selbst
|
||
beidhaelftig tragen
|
||
(4) Mehrfachvergleich wird ausgewiesen: bei ~10 Tests sind ~0,5 Zufallstreffer
|
||
zu erwarten — ein einzelnes ✅ ist damit schwach.
|
||
|
||
Aufruf: python backtest_verworfene_v2.py [bars]
|
||
"""
|
||
import sys
|
||
from datetime import datetime, timedelta, timezone
|
||
from zoneinfo import ZoneInfo
|
||
|
||
import MetaTrader5 as mt5
|
||
|
||
import core.wave_rec as wr
|
||
from backtest_cost_gate import _atr_series, kennzahlen
|
||
from core.exit_model import LIVE, simulate
|
||
|
||
_BERLIN = ZoneInfo("Europe/Berlin")
|
||
_BROKER = timezone(timedelta(hours=3))
|
||
_UTC = timezone.utc
|
||
_ATRMIN = 0.06
|
||
_MIN_N = 40
|
||
|
||
|
||
def _berlin(ts):
|
||
return (datetime.fromtimestamp(int(ts), _UTC)
|
||
.replace(tzinfo=_BROKER).astimezone(_BERLIN))
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# Setups: jedes liefert STEHENDE Level als (ab_bar, bis_bar, level, richtung)
|
||
# richtung: +1 = Ausbruch nach oben ist LONG, −1 = nach unten ist SHORT
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
def orb(H, L, T, A, start_h, start_m, minuten):
|
||
"""Opening Range Breakout: Box = erste `minuten` nach dem Open, gilt bis 22:00."""
|
||
out, tag, box = [], None, None
|
||
for i in range(len(H)):
|
||
b = _berlin(T[i])
|
||
d = b.date()
|
||
if d != tag:
|
||
tag, box = d, None
|
||
beginn = b.replace(hour=start_h, minute=start_m, second=0, microsecond=0)
|
||
ende = beginn + timedelta(minutes=minuten)
|
||
if box is None and b >= ende:
|
||
js = [j for j in range(max(0, i - 40), i)
|
||
if beginn <= _berlin(T[j]) < ende]
|
||
if js:
|
||
hi, lo = max(H[j] for j in js), min(L[j] for j in js)
|
||
bis = i
|
||
while bis < len(H) - 2 and _berlin(T[bis]).date() == d \
|
||
and _berlin(T[bis]).hour < 22:
|
||
bis += 1
|
||
out.append((i, bis, hi, +1))
|
||
out.append((i, bis, lo, -1))
|
||
box = True
|
||
return out
|
||
|
||
|
||
def tagesextrem(H, L, T):
|
||
"""PDH/PDL — Hoch/Tief des Vortages, gilt den ganzen Folgetag."""
|
||
tage = {}
|
||
for i in range(len(H)):
|
||
d = _berlin(T[i]).date()
|
||
r = tage.setdefault(d, [i, i, H[i], L[i]])
|
||
r[1] = i; r[2] = max(r[2], H[i]); r[3] = min(r[3], L[i])
|
||
folge = sorted(tage)
|
||
out = []
|
||
for k in range(1, len(folge)):
|
||
_, _, hi, lo = tage[folge[k - 1]]
|
||
a, b, _, _ = tage[folge[k]]
|
||
out.append((a, b, hi, +1))
|
||
out.append((a, b, lo, -1))
|
||
return out
|
||
|
||
|
||
def asia(H, L, T):
|
||
"""Asian Range 00:00–08:00 Berlin, gilt 08:00 bis Tagesende."""
|
||
out, tag, gesammelt = [], None, []
|
||
for i in range(len(H)):
|
||
b = _berlin(T[i])
|
||
if b.date() != tag:
|
||
tag, gesammelt, gesetzt = b.date(), [], False
|
||
if b.hour < 8:
|
||
gesammelt.append(i)
|
||
elif gesammelt and not globals().get("_dummy"):
|
||
if b.hour == 8 and b.minute < 5:
|
||
hi = max(H[j] for j in gesammelt); lo = min(L[j] for j in gesammelt)
|
||
bis = i
|
||
while bis < len(H) - 2 and _berlin(T[bis]).date() == tag:
|
||
bis += 1
|
||
out.append((i, bis, hi, +1))
|
||
out.append((i, bis, lo, -1))
|
||
gesammelt = []
|
||
return out
|
||
|
||
|
||
def equal_hl(H, L, A, tol=0.15, gueltig=120):
|
||
"""Equal Highs/Lows aus M5-Pivots (k=3) — zwei Extrema innerhalb tol×ATR."""
|
||
ph = [i for i in range(3, len(H) - 3)
|
||
if H[i] == max(H[i - 3:i + 4])]
|
||
pl = [i for i in range(3, len(L) - 3)
|
||
if L[i] == min(L[i - 3:i + 4])]
|
||
out = []
|
||
for piv, arr, d in ((ph, H, +1), (pl, L, -1)):
|
||
for a in range(1, len(piv)):
|
||
i0, i1 = piv[a - 1], piv[a]
|
||
if not (6 <= i1 - i0 <= 240):
|
||
continue
|
||
atr = A[i1]
|
||
if not atr or atr < _ATRMIN:
|
||
continue
|
||
if abs(arr[i1] - arr[i0]) > tol * atr:
|
||
continue
|
||
lvl = (arr[i1] + arr[i0]) / 2.0
|
||
out.append((i1 + 4, min(i1 + 4 + gueltig, len(H) - 2), lvl, d))
|
||
return out
|
||
|
||
|
||
def squeeze_kontrolle(H, L, C, A):
|
||
"""Kontrolle: die Squeeze-Box als stehendes Level (muss +0,124/+0,292 treffen)."""
|
||
out = []
|
||
for i in range(wr._SQ_N + 2, len(C) - 2):
|
||
atr = A[i - 1]
|
||
if not atr or atr < _ATRMIN:
|
||
continue
|
||
hb = H[i - wr._SQ_N - 1:i - 1]; lb = L[i - wr._SQ_N - 1:i - 1]
|
||
if not hb or not lb:
|
||
continue
|
||
hi, lo = max(hb), min(lb)
|
||
if (hi - lo) / atr > wr._SQ_MULT:
|
||
continue
|
||
out.append((i, i, hi + wr._SQ_K * atr, +1))
|
||
out.append((i, i, lo - wr._SQ_K * atr, -1))
|
||
return out
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
def lauf(levels, H, L, C, SP, A, lo, hi, modus):
|
||
"""modus: 'markt' = Bar-Close · 'close' = Level, aber nur bei Close-Bruch
|
||
· 'touch' = Level, Fill bei erster Beruehrung (REALITAET)."""
|
||
ereignisse = []
|
||
for ab, bis, lvl, d in levels:
|
||
for i in range(max(ab, 1), min(bis + 1, len(C) - 2)):
|
||
atr = A[i - 1]
|
||
if not atr or atr < _ATRMIN:
|
||
continue
|
||
beruehrt = (H[i] >= lvl) if d > 0 else (L[i] <= lvl)
|
||
if not beruehrt:
|
||
continue
|
||
gebrochen = (C[i] > lvl) if d > 0 else (C[i] < lvl)
|
||
if modus != "touch" and not gebrochen:
|
||
break # Level verbraucht (Docht) — kein Signal
|
||
entry = C[i] if modus == "markt" else lvl
|
||
ereignisse.append((i, d, entry, atr, SP[i]))
|
||
break # ein Fill je Level
|
||
ereignisse.sort()
|
||
rs, frei_ab = [], lo
|
||
for i, d, entry, atr, sp in ereignisse:
|
||
if not (lo <= i < hi) or i < frei_ab:
|
||
continue
|
||
r, xb = simulate(entry, d, atr, H, L, C, i + 1, LIVE,
|
||
timestop=True, use_tp=True, ret_bar=True)
|
||
rs.append(r - (sp if sp > 0 else 0.0225) / atr)
|
||
frei_ab = xb + 1
|
||
return rs
|
||
|
||
|
||
def main():
|
||
n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000
|
||
if not mt5.initialize():
|
||
print("MT5 nicht erreichbar"); return 1
|
||
sym = next((c for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD")
|
||
if mt5.symbol_info(c)), None)
|
||
b = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n)
|
||
point = mt5.symbol_info(sym).point
|
||
mt5.shutdown()
|
||
H = [float(x["high"]) for x in b]; L = [float(x["low"]) for x in b]
|
||
C = [float(x["close"]) for x in b]; T = [int(x["time"]) for x in b]
|
||
SP = [float(x["spread"]) * point for x in b]
|
||
A = _atr_series(H, L, C)
|
||
m = len(C) // 2
|
||
|
||
setups = [
|
||
("KONTROLLE Squeeze", squeeze_kontrolle(H, L, C, A)),
|
||
("ORB EU 15 min", orb(H, L, T, A, 10, 0, 15)),
|
||
("ORB EU 30 min", orb(H, L, T, A, 10, 0, 30)),
|
||
("ORB US 15 min", orb(H, L, T, A, 16, 30, 15)),
|
||
("ORB US 30 min", orb(H, L, T, A, 16, 30, 30)),
|
||
("PDH / PDL Bruch", tagesextrem(H, L, T)),
|
||
("Asian Range Bruch", asia(H, L, T)),
|
||
("Equal H/L Bruch", equal_hl(H, L, A)),
|
||
]
|
||
|
||
print("=" * 104)
|
||
print(f" VERWORFENE SETUPS — neu auf der Ausfuehrungs-Achse ({sym}, {len(C)} M5-Bars)")
|
||
print("=" * 104)
|
||
print(" Geprueft wird NUR, was ein STEHENDES Level + Ausbruchsrichtung hat —")
|
||
print(" dort kann die Achse ueberhaupt wirken (Squeeze ja, Momentum nein).")
|
||
print(" Massgeblich ist Spalte C. B steht nur da, um den Look-ahead zu zeigen.")
|
||
print()
|
||
print(f" {'Setup':<22} {'A) Markt H1/H2':^22} {'B) Close-Bruch H1/H2':^24} "
|
||
f"{'C) BERUEHRUNG H1/H2':^24} n Urteil")
|
||
print(" " + "-" * 100)
|
||
|
||
treffer = []
|
||
for name, lv in setups:
|
||
z = f" {name:<22}"
|
||
erg = {}
|
||
for modus in ("markt", "close", "touch"):
|
||
k1 = kennzahlen(lauf(lv, H, L, C, SP, A, 0, m, modus))
|
||
k2 = kennzahlen(lauf(lv, H, L, C, SP, A, m, len(C), modus))
|
||
erg[modus] = (k1, k2)
|
||
z += (f" {k1['oer']:>+7.3f}/{k2['oer']:>+7.3f} " if k1 and k2
|
||
else f" {'—':^16} ")
|
||
c1, c2 = erg["touch"]
|
||
if not c1 or not c2:
|
||
print(z + " —"); continue
|
||
ok = (c1["oer"] > 0 and c2["oer"] > 0 and c1["pf"] > 1 and c2["pf"] > 1
|
||
and c1["n"] >= _MIN_N and c2["n"] >= _MIN_N)
|
||
z += f" {c1['n']:>4} {'✅' if ok else '❌'}"
|
||
if c1["n"] < _MIN_N or c2["n"] < _MIN_N:
|
||
z += " (duenn)"
|
||
print(z)
|
||
# ⚠⚠ 95-%-KI der ENTSCHEIDENDEN Spalte (C, Beruehrungs-Fill). Enthaelt es
|
||
# die Null, ist das Vorzeichen NICHT gesichert — und genau darauf ruht die
|
||
# ganze Entscheidung. Ohne diese Zahl sah „+0,142 / +0,134" nach einem
|
||
# Befund aus; mit ihr ist sofort sichtbar, ob n ausreicht.
|
||
unsicher = []
|
||
for k, tag in ((c1, "H1"), (c2, "H2")):
|
||
if k.get("ci"):
|
||
lo, hi = k["ci"]
|
||
unsicher.append(f"{tag} [{lo:+.3f}…{hi:+.3f}]"
|
||
+ ("⚠0" if lo <= 0 <= hi else ""))
|
||
if unsicher:
|
||
print(f" └ 95-%-KI: {' '.join(unsicher)}")
|
||
if ok and not name.startswith("KONTROLLE"):
|
||
treffer.append(name)
|
||
|
||
print()
|
||
print("=" * 104)
|
||
print(f" ERGEBNIS: {len(treffer)} von {len(setups)-1} verworfenen Setups tragen "
|
||
f"auch mit realistischem Fill")
|
||
if treffer:
|
||
print(f" → {', '.join(treffer)}")
|
||
print(" ⚠ Vor jeder Konsequenz: Nachbar-Parameter pruefen und gegen die")
|
||
print(" Kontrolle halten. Ein einzelnes ✅ bei ~7 Tests ist erwartbar.")
|
||
else:
|
||
print(" → Die Ablehnungen bleiben bestehen. Die Ausfuehrungs-Achse rettet")
|
||
print(" keines dieser Setups.")
|
||
print()
|
||
print(" ⚠ Spalte B gegen C gelesen = der Look-ahead-Aufschlag. Ist er gross,")
|
||
print(" war das Setup nie so gut, wie ein Level-Backtest suggeriert.")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|