Files
AH-Oil-Trader/backtest_verworfene_v2.py
T
Axel HocksandClaude Opus 5 79f3e8efe1 Konfidenzintervalle (scipy-Bootstrap) — und sie korrigieren den Befund vom 06.08.
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>
2026-08-07 00:25:02 +02:00

295 lines
12 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
"""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:0008: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())