diff --git a/CLAUDE.md b/CLAUDE.md index 8383c34..ad2f15a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4942,6 +4942,47 @@ Modell schliesst keine Trades. „die Rangfolge ist kaputt" ist **nicht** belegt; belegt ist nur die Basisraten-/Kalibrierungslücke. +## ⚠⚠ DIE ZWEI GERATENEN M15-SCHWELLEN GEMESSEN — beide NICHT kalibrierbar (2026-08-08) + +Auftrag: die Reichweite (0,5×ATR) und die Kosten-Schwelle (0,32, von M5) messen +statt raten. `backtest_m15_gates.py`, 2 Halbjahre, Level-Berührungen auf M15, +Einstieg AM Level in HTF-Richtung, **Fill bei Berührung**, kanonischer Exit, +Echtkosten, Episoden-entkoppelt, mit 95-%-Bootstrap je Zelle. + +⚠⚠ **ZUERST EIN ARTEFAKT IM EIGENEN CODE — und es hätte zu einem absurden Rat +geführt.** Der erste Lauf stieg **sofort zum Level-Preis** ein, auch wenn der +Kurs 0,9×ATR entfernt stand — die Sim schenkte sich damit genau diesen Abstand. +Ergebnis: „je WEITER weg vom Level, desto besser" (oberster Bucket +0,293 in H2, +KI schloss die Null aus). Mit **Fill bei Berührung** verschwindet es: +| Abstand | vorher H2 | **mit Fill** | +|---|---|---| +| 0,30–0,50 | +0,142 | +0,216 | +| 0,75–1,00 | **+0,293** | **+0,179** | +Derselbe Fehler wie beim Momentum-Test (ØR +2,2 entlarvte den Code). + +**② ERGEBNIS: KEINE der beiden Schwellen ist kalibrierbar.** +| Abstand ×ATR | H1 ØR | H2 ØR | +|---|---|---| +| 0,00–0,10 | −0,227 | +0,100 | +| 0,10–0,20 | −0,130 | −0,115 | +| 0,20–0,30 | −0,306 | −0,129 | +| 0,30–0,50 | −0,112 | **+0,216** | +| 0,50–0,75 | −0,039 | +0,042 | +| 0,75–1,00 | −0,040 | +0,179 | +**H1 ist über ALLE sechs Abstands-Buckets negativ.** Kein Bucket besteht die +vorab fixierte Regel (beidhälftig positiv + KI ohne Null). Beim Kosten-Gate +dasselbe: nur H2 0,06–0,10 schliesst die Null aus (+0,254/+0,307), H1 liegt dort +bei +0,016/−0,040. + +✅ **Der eigentliche Befund ist nicht „welche Schwelle", sondern: es gibt nichts +zu optimieren.** Die Population selbst trägt in H1 nicht — Level-Berührungen auf +M15 in HTF-Richtung sind dort durchgehend negativ. Eine Schwelle kann keinen +Edge erzeugen, den die Grundgesamtheit nicht hat. +⚠ Konsequenz für die Karte: die 0,5×ATR bleiben eine **gesetzte** Zahl (ehrlich +so benannt), und das Kosten-Gate bleibt auf M15 **dekorativ** — beides ist jetzt +gemessen, nicht mehr nur vermutet. Einzige schwach gestützte Richtung: teuer ist +schlechter (H2 0,16–0,20 → −0,482, KI [−0,949 … +0,008]). + ## ⚠⚠ M15-KARTE AN 7 TAGEN GETESTET — zwei eigene Fehler, einer davon fast veröffentlicht (2026-08-08) **(1) BIAS WAR EIN RICHTUNGSGEBER — korrigiert zum VETO (v=155).** Der Tagestest diff --git a/backtest_m15_gates.py b/backtest_m15_gates.py new file mode 100644 index 0000000..e2a2578 --- /dev/null +++ b/backtest_m15_gates.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +"""Die zwei GERATENEN Schwellen der M15-Karte messen. + +ANLASS (2026-08-08). Der Wochentest hat zwei Schwächen offengelegt: + (1) **Reichweite 0,5×ATR** — von mir GESETZT, nie gemessen. Sie ist zugleich + der EINZIGE Gate, der überhaupt diskriminiert (das Kosten-Gate blockte + 0 von 644 Bars, das Veto trägt nur +0,04 R). + (2) **Kosten-Schwelle 0,32** — stammt von M5 (`backtest_realcosts.py`: + Ø 0,265×ATR, Nacht 0,32–0,50 = Kostenfalle). Auf M15 liegt Spread/ATR + bei ~0,10, die Schwelle bindet dort NIE. + +Beides wird hier über 2 Halbjahre auf der ECHTEN Population gemessen: +Level-Berührungen auf M15, Einstieg AM Level in der vom HTF erlaubten Richtung, +kanonischer Exit, Echtkosten, Episoden-entkoppelt (nur die erste Bar je +zusammenhängendem Block — sonst zählt man dieselbe Marktlage vielfach, der +Fehler aus dem ersten Tagestest). + +⚠⚠ ENTSCHEIDUNGSREGEL, VORAB: + · Eine Schwelle wird nur übernommen, wenn der Vorteil in BEIDEN Hälften + besteht UND die Nachbar-Schwellen nicht kippen. + · Zusätzlich das 95-%-Bootstrap-Intervall — bei den kleinen Effekten dieser + Population (ØR-Bereich ±0,1) ist ein Punktschätzer keine Aussage. Genau + daran ist heute schon die Slippage-Regel gescheitert. + +Aufruf: python backtest_m15_gates.py [n_M15_bars] (Default 26000) +""" +from __future__ import annotations + +import random +import sys + +import MetaTrader5 as mt5 + +from core.exit_model import LIVE, simulate +from core.sr_levels import halten, linien, naechste +from test_m15_karte_tag import atr_series, htf_sign, m30_levels + +_REICH_STUFEN = (0.1, 0.2, 0.3, 0.5, 0.75, 1.0) +_KOST_STUFEN = (0.06, 0.08, 0.10, 0.13, 0.16, 0.20) + + +def sammle(H, L, C, SP, A, S30, LV, lo, hi): + """Alle Level-Berührungen als Episoden — mit gap und Kostenquote je Fall.""" + out = [] + prev_r = prev_s = None + vor = False + for i in range(lo, min(hi, len(C) - LIVE.max_hold - 2)): + a = A[i] + if not a or not LV[i]: + vor = False; continue + ph, pl = LV[i] + lv = linien(ph, pl, a) + r, s = naechste(lv, C[i]) + r = halten(r, prev_r, C[i], a, True) + s = halten(s, prev_s, C[i], a, False) + prev_r, prev_s = r, s + gaps = [(abs(x - C[i]) / a, x) for x in (r, s) if x is not None] + if not gaps: + vor = False; continue + g, lvl = min(gaps) + # „am Level" im weitesten geprueften Sinn — die Schwelle wird UNTEN variiert + nah = g <= max(_REICH_STUFEN) + if nah and not vor and S30[i]: + d = 1 if S30[i] == 1 else -1 + out.append({"gap": g, "kost": SP[i] / a, "i": i, "d": d, + "lvl": lvl, "atr": a}) + vor = nah + return out + + +def rechne(eintraege, H, L, C, warte=8): + """⚠⚠ FILL BEI BERUEHRUNG — nicht sofort zum Level-Preis. + + Der erste Entwurf stieg direkt bei `lvl` ein, auch wenn der Kurs 0,9×ATR + entfernt stand. Das schenkt der Sim einen um genau diesen Abstand besseren + Einstieg und erzeugte das Scheinergebnis „weiter weg ist besser" (ØR +0,29 + im obersten Abstands-Bucket). Derselbe Fehler wie beim Momentum-Test, wo + ein ØR von +2,2 den Code entlarvte: **eine Zahl, die zu gut ist, ist ein + Befund ueber den Code, nicht ueber den Markt.** + + Jetzt liegt die Order `warte` Bars am Level; wird sie nicht beruehrt, + faellt der Fall raus (`R = None`).""" + for e in eintraege: + i, lvl, d = e["i"], e["lvl"], e["d"] + fill = None + for j in range(i + 1, min(i + 1 + warte, len(C) - LIVE.max_hold - 2)): + if L[j] <= lvl <= H[j]: + fill = j; break + e["R"] = (None if fill is None else + simulate(lvl, d, e["atr"], H, L, C, fill + 1, LIVE) - e["kost"]) + return [e for e in eintraege if e["R"] is not None] + + +def ci(Rs, n=3000, seed=42): + if len(Rs) < 20: + return None, None + r = random.Random(seed); m = len(Rs) + b = [sum(Rs[r.randrange(m)] for _ in range(m)) / m for _ in range(n)] + b.sort(); return b[int(.025 * n)], b[int(.975 * n)] + + +def tabelle(titel, h1, h2, schluessel, stufen, kanten_text): + print(f"\n {titel}") + print(f" {kanten_text:<16}{'H1 n':>7}{'H1 ØR':>9}{'H1 95-%-KI':>22}" + f"{'H2 n':>7}{'H2 ØR':>9}{'H2 95-%-KI':>22}") + for lo_s, hi_s in zip((0,) + stufen[:-1], stufen): + z = [] + for teil in (h1, h2): + grp = [e["R"] for e in teil if lo_s <= e[schluessel] < hi_s] + if len(grp) < 20: + z += [str(len(grp)), "—", "—"]; continue + m = sum(grp) / len(grp); a, b = ci(grp) + z += [str(len(grp)), f"{m:+.3f}", + f"[{a:+.3f} … {b:+.3f}]" if a is not None else "—"] + print(f" {f'{lo_s:.2f}–{hi_s:.2f}':<16}{z[0]:>7}{z[1]:>9}{z[2]:>22}" + f"{z[3]:>7}{z[4]:>9}{z[5]:>22}") + + +def main(): + n = int(sys.argv[1]) if len(sys.argv) > 1 else 26000 + 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_M15, 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) + S30 = htf_sign(H, L, C, 2) + LV = m30_levels(H, L, C) + mid = len(C) // 2 + + r1 = sammle(H, L, C, SP, A, S30, LV, 60, mid) + r2 = sammle(H, L, C, SP, A, S30, LV, mid, len(C)) + h1 = rechne(r1, H, L, C); h2 = rechne(r2, H, L, C) + print(f" Fill-Quote: H1 {len(h1)}/{len(r1)} = {100*len(h1)/max(1,len(r1)):.0f} %" + f" H2 {len(h2)}/{len(r2)} = {100*len(h2)/max(1,len(r2)):.0f} %") + + print("=" * 100) + print(f" M15-GATES MESSEN — {sym}, {len(C)} Bars H1 n={len(h1)} H2 n={len(h2)}") + print(" Population: Level-Berührungen, Einstieg AM Level in HTF-Richtung,") + print(" kanonischer Exit, Echtkosten, Episoden-entkoppelt.") + print("=" * 100) + + tabelle("① REICHWEITE — ab welchem Abstand trägt der Einstieg noch?", + h1, h2, "gap", _REICH_STUFEN, "Abstand ×ATR") + tabelle("② KOSTEN — ab welcher Quote wird es teuer? (M5-Schwelle 0,32 bindet nie)", + h1, h2, "kost", _KOST_STUFEN, "Spread/ATR") + + ks = sorted(e["kost"] for e in h1 + h2) + gs = sorted(e["gap"] for e in h1 + h2) + print(f"\n Verteilungen: Spread/ATR Median {ks[len(ks)//2]:.3f} " + f"(90-Perz. {ks[int(0.9*len(ks))]:.3f}) · " + f"Abstand Median {gs[len(gs)//2]:.2f}×ATR") + print("\n ⚠ Übernommen wird eine Schwelle nur, wenn der Vorteil in BEIDEN") + print(" Hälften besteht, die Nachbarn nicht kippen UND das KI die Null") + print(" ausschliesst. Punktschätzer allein sind auf dieser Population keine Aussage.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())