Auftrag: Reichweite (0,5xATR) und Kosten-Schwelle (0,32, von M5) messen statt raten. 2 Halbjahre, Level-Beruehrungen M15, Fill bei Beruehrung, kanonischer Exit, Echtkosten, Episoden-entkoppelt, mit Bootstrap je Zelle. ZUERST EIN ARTEFAKT IM EIGENEN CODE: der erste Lauf stieg sofort zum Level-Preis ein, auch wenn der Kurs 0,9xATR entfernt stand - die Sim schenkte sich diesen Abstand. Ergebnis war "je weiter weg, desto besser" (+0,293 im obersten Bucket, KI ohne Null). Mit Fill bei Beruehrung faellt es auf +0,179. Derselbe Fehler wie beim Momentum-Test, wo OeR +2,2 den Code entlarvte. ERGEBNIS: keine der beiden Schwellen ist kalibrierbar. Abstand xATR H1 OeR H2 OeR 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 ueber ALLE sechs Buckets negativ. Kein Bucket besteht die vorab fixierte Regel. Beim Kosten-Gate dasselbe: nur H2 0,06-0,10 schliesst die Null aus, H1 liegt dort bei +0,016/-0,040. Der eigentliche Befund ist nicht "welche Schwelle", sondern: es gibt nichts zu optimieren. Level-Beruehrungen auf M15 in HTF-Richtung sind in H1 durchgehend negativ. Eine Schwelle kann keinen Edge erzeugen, den die Grundgesamtheit nicht hat. Konsequenz: die 0,5xATR bleiben eine gesetzte Zahl (ehrlich so benannt), das Kosten-Gate bleibt auf M15 dekorativ - beides jetzt gemessen statt vermutet. Einzige schwach gestuetzte Richtung: teuer ist schlechter (H2 0,16-0,20 -> -0,482, KI [-0,949 ... +0,008]). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
165 lines
6.8 KiB
Python
165 lines
6.8 KiB
Python
#!/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())
|