#!/usr/bin/env python3 """Kann der Autotrader die M15-KARTE automatisch handeln? (User-Auftrag 2026-08-13) FRAGE: Der Bot soll eroeffnen, sobald die Karte GRUEN steht und eine gerichtete Lesart zeigt. Das ist eine **Track-B-Frage** (Signal/Edge) — hohe Beweislast. ⚠⚠ WAS HIER NEU IST. Die Karte wurde ausdruecklich als BEDINGUNGS-Anzeige gebaut, nicht als Richtungsquelle. Ihre Richtung entsteht erst aus der `lesart`, und die ist bisher NIE als Handelsregel gemessen worden: · `backtest_m15_gates.py` (08.08.) handelt die vom HTF ERLAUBTE Richtung — nicht die Lesart. Ergebnis dort: H1 ueber ALLE sechs Abstands-Buckets negativ. · `backtest_squeeze_m15.py` / `backtest_vwap_m15.py` messen andere Signale. Neu gemessen wird also genau der Zusatz: **traegt die P(break)-Lesart als Richtungsgeber?** VORAB FIXIERTE ENTSCHEIDUNGSREGEL (damit die Latte hinterher nicht wandert): (1) OeR > 0 UND PF > 1 in BEIDEN Haelften (2) 95-%-Bootstrap-KI schliesst in BEIDEN Haelften die Null aus (3) n >= 100 je Haelfte (4) schlaegt die Kontrolle C (HTF-Richtung) in BEIDEN Haelften (5) Nachbar-Schwellen (gap 0,4/0,5/0,6) kippen das Vorzeichen nicht Verfehlt eine Bedingung → NICHT bauen. AUFBAU — live-treu, mit den GETEILTEN Kernen statt Nachbauten: · Bars M5 (die Ebene, auf der P(break) trainiert ist — auf M15 waere das Modell falsch skaliert, Fix 2026-07-13). · ATR_M15 / M30-Level / HTF-Trend durch Aggregation aus M5 (3 / 6 / 12). · Level ueber `core.sr_levels` (linien/naechste/halten) = DERSELBE Code wie `_draw_levels` — der Nachbau wich am 07.08. nachweislich ab. · P(break) ueber `core.engine._p_break` mit den echten Live-Gewichten. · Status/Lesart inkl. der **Hysterese** vom 12.08. (sonst misst man das Flattern statt der Regel). · Exit `core.exit_model.LIVE`, Echtkosten aus dem Bar-Spread. · Sequentiell EIN Slot, 1x je Episode (wie der Live-Dedup). · Fill bei BERUEHRUNG — nicht zum Level-Preis, egal wo der Bar schliesst. Der Look-ahead-Aufschlag lag im Projekt bei 0,32-0,85 R. Aufruf: python backtest_m15_auto.py [bars] """ from __future__ import annotations import random import sys from bisect import bisect_left, bisect_right import MetaTrader5 as mt5 from core.engine import _p_break from core.exit_model import LIVE, simulate from core.sr_levels import halten, linien, naechste from test_m15_karte_tag import atr_series, ema _PIV_K = 3 _LOOK = 50 # M30-Pivot-Fenster wie live _HTF_DEAD = 0.15 _ATR5_FLOOR = 0.12 # wie im P(break)-Training _WARTE = 8 # Bars, die die ruhende Order am Level liegt def agg(H, L, C, step): """Hoehere TF aus M5 aggregieren (nur abgeschlossene Bars).""" hh, hl, hc = [], [], [] for s in range(0, len(C) - step + 1, step): hh.append(max(H[s:s + step])); hl.append(min(L[s:s + step])) hc.append(C[s + step - 1]) return hh, hl, hc def htf_sign(H, L, C, step): """Trend-Vorzeichen der hoeheren TF je M5-Index — LOOK-AHEAD-frei.""" hh, hl, hc = agg(H, L, C, step) out = [0] * len(C) if len(hc) < 55: return out ef, es, ha = ema(hc, 12), ema(hc, 50), atr_series(hh, hl, hc) sg = [] for j in range(len(hc)): a = ha[j] if j < 52 or not a: sg.append(0); continue d = ef[j] - es[j] sg.append(0 if abs(d) < _HTF_DEAD * a else (1 if d > 0 else -1)) for i in range(len(C)): j = i // step - 1 out[i] = sg[j] if 0 <= j < len(sg) else 0 return out def atr_hoeher(H, L, C, step): """ATR der hoeheren TF je M5-Index (kausal: letzter ABGESCHLOSSENER Bar).""" hh, hl, hc = agg(H, L, C, step) ha = atr_series(hh, hl, hc) out = [None] * len(C) for i in range(len(C)): j = i // step - 1 out[i] = ha[j] if 0 <= j < len(ha) else None return out def pivots(H, L, C, step): """ALLE M30-Pivots EINMAL berechnen (Index + Preis), statt je Bar neu. ⚠ Das ist die dokumentierte O(log n)-Form: die quadratische Fassung war der Engpass des Projekts (9 min → 8,9 s). Das Pivot-Kriterium ist LOKAL, eine globale Berechnung liefert daher dieselbe Menge wie das rollende Fenster. Kausal: ein Pivot bei x ist erst bei x+K bekannt. """ hh, hl, _ = agg(H, L, C, step) ph_i, ph_v, pl_i, pl_v = [], [], [], [] for x in range(_PIV_K, len(hh) - _PIV_K): if hh[x] == max(hh[x - _PIV_K:x + _PIV_K + 1]): ph_i.append(x); ph_v.append(round(hh[x], 3)) if hl[x] == min(hl[x - _PIV_K:x + _PIV_K + 1]): pl_i.append(x); pl_v.append(round(hl[x], 3)) return (ph_i, ph_v), (pl_i, pl_v) def fenster(idx, val, lo, hi): """Pivots mit lo <= index <= hi — zwei Binaersuchen statt Listenlauf.""" a, b = bisect_left(idx, lo), bisect_right(idx, hi) return val[a:b] def lauf(H, L, C, SP, lo, hi, gap_gruen, modus): """Sequentielle Sim EINES Slots ueber [lo, hi). modus: 'karte' = Richtung aus der Lesart (das, was gefragt ist) 'veto' = Lesart, aber nur wenn sie der HTF-Erlaubnis entspricht 'htf' = KONTROLLE: HTF-erlaubte Richtung (= backtest_m15_gates) 'gegen' = KONTROLLE: Gegenrichtung der Lesart """ a5 = atr_series(H, L, C) a15 = atr_hoeher(H, L, C, 3) s30 = htf_sign(H, L, C, 6) (ph_i, ph_v), (pl_i, pl_v) = pivots(H, L, C, 6) ef, es = ema(C, 12), ema(C, 50) Rs, n_ep = [], 0 prev_r = prev_s = None hy_status, hy_lesart = None, None arm = None # letzte gehandelte Episode (dir, level) frei_ab = lo # Slot belegt bis ... grenze = min(hi, len(C) - LIVE.max_hold - 2) for i in range(lo, grenze): a = a5[i]; a15i = a15[i] if not a or not a15i: continue a5f = max(a, _ATR5_FLOOR) j30 = i // 6 - 1 if j30 < _PIV_K + 5: continue ph = fenster(ph_i, ph_v, j30 - _LOOK, j30 - _PIV_K) pl = fenster(pl_i, pl_v, j30 - _LOOK, j30 - _PIV_K) if not ph and not pl: continue # --- Level: DERSELBE Code wie _draw_levels (Clustern mit ATR_M5) --- lv = linien(ph, pl, a5f) r, s = naechste(lv, C[i]) r = halten(r, prev_r, C[i], a5f, True) s = halten(s, prev_s, C[i], a5f, False) prev_r, prev_s = r, s # --- Zonen + P(break), Merkmale trainingsgleich aus M5 --- mom6 = (C[i] - C[i - 6]) / a5f mom3 = (C[i] - C[i - 3]) / a5f ediff = (ef[i] - es[i]) / a5f zonen = [] for lvl, seite in ((r, "res"), (s, "sup")): if lvl is None: continue d = 1 if seite == "res" else -1 g = abs(lvl - C[i]) wt = 1.0 if ediff * d > 0 else 0.0 p = _p_break(mom6 * d, mom3 * d, wt, g / a5f) zonen.append({"seite": seite, "level": lvl, "gap15": g / a15i, "pb": round(100 * p)}) if not zonen: continue nah = min(zonen, key=lambda x: x["gap15"]) # --- Status + Lesart MIT der Live-Hysterese (12.08.) --- kost = SP[i] / a15i if kost >= 0.32: status = "rot" else: schw = (gap_gruen + 0.10) if hy_status == "gruen" else gap_gruen status = "gruen" if nah["gap15"] <= schw else "gelb" lesart = "unklar" pb = nah["pb"] war_ger = hy_lesart in ("auf", "ab") lo_s, hi_s = (45, 55) if war_ger else (40, 60) if pb < lo_s or pb > hi_s: bricht = pb > 55 if war_ger else pb > hi_s res = nah["seite"] == "res" lesart = "auf" if (res if bricht else not res) else "ab" elif war_ger: lesart = hy_lesart hy_status, hy_lesart = status, lesart # --- Episoden-Dedup wie live: 1x je gruener, gerichteter Episode --- if status != "gruen" or lesart == "unklar": arm = None continue d_les = 1 if lesart == "auf" else -1 kennung = (d_les, nah["level"]) if arm == kennung: continue arm = kennung n_ep += 1 if i < frei_ab: # Slot belegt — Episode zaehlt, kein Trade continue # --- Richtung je Modus --- erlaubt = s30[i] if modus == "karte": d = d_les elif modus == "gegen": d = -d_les elif modus == "veto": if not erlaubt or erlaubt != d_les: continue d = d_les else: # 'htf' if not erlaubt: continue d = erlaubt # --- Fill bei BERUEHRUNG, dann kanonischer Exit --- lvl = nah["level"] fill = None for k in range(i + 1, min(i + 1 + _WARTE, grenze)): if L[k] <= lvl <= H[k]: fill = k; break if fill is None: continue R, xb = simulate(lvl, d, a5f, H, L, C, fill + 1, LIVE, ret_bar=True) Rs.append(R - SP[i] / a5f) frei_ab = xb + 1 return Rs, n_ep def ci(Rs, n=3000, seed=42): if len(Rs) < 20: return None, None r = random.Random(seed); m = len(Rs) b = sorted(sum(Rs[r.randrange(m)] for _ in range(m)) / m for _ in range(n)) return b[int(.025 * n)], b[int(.975 * n)] def kenn(Rs): if not Rs: return None g = sum(x for x in Rs if x > 0); v = -sum(x for x in Rs if x < 0) u, o = ci(Rs) return {"n": len(Rs), "oer": sum(Rs) / len(Rs), "sr": sum(Rs), "wr": 100 * sum(1 for x in Rs if x > 0) / len(Rs), "pf": (g / v if v else float("inf")), "u": u, "o": o} def zeile(name, k): if not k: return f" {name:<34} —" kib = " — " if k["u"] is None else f"[{k['u']:+.3f} … {k['o']:+.3f}]" null = "" if k["u"] is None else ("" if (k["u"] > 0 or k["o"] < 0) else " ⚠ KI enthaelt 0") return (f" {name:<34} n={k['n']:>4} WR {k['wr']:>4.1f} % OeR {k['oer']:>+.3f} " f"PF {k['pf']:>5.2f} SR {k['sr']:>+8.1f} {kib}{null}") 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() if bars is None: print(" keine Bars"); return 1 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] mid = len(C) // 2 print(f"\n {sym} · {len(C)} M5-Bars · Exit kanonisch · Echtkosten · " f"Fill bei Beruehrung · EIN Slot") print(f" Regel vorab fixiert: OeR>0 & PF>1 & KI ohne 0 in BEIDEN Haelften, " f"n>=100, schlaegt Kontrolle C, Nachbarn kippen nicht\n") print(" ── HAUPTFRAGE: die Karte automatisch handeln (gap<=0,50) ──────") erg = {} for modus, name in (("karte", "A) Karte (Lesart)"), ("veto", "B) Lesart + Veto-konform"), ("htf", "C) KONTROLLE HTF-Richtung"), ("gegen", "D) KONTROLLE Gegenrichtung")): r1, e1 = lauf(H, L, C, SP, 60, mid, 0.50, modus) r2, e2 = lauf(H, L, C, SP, mid, len(C), 0.50, modus) erg[modus] = (kenn(r1), kenn(r2)) print(zeile(name + " H1", erg[modus][0])) print(zeile(name + " H2", erg[modus][1])) if modus == "karte": print(f" (gruene gerichtete Episoden: H1 {e1} · H2 {e2})") print() print(" ── NACHBAR-SCHWELLEN (Bedingung 5) ────────────────────────────") for g in (0.40, 0.50, 0.60): r1, _ = lauf(H, L, C, SP, 60, mid, g, "karte") r2, _ = lauf(H, L, C, SP, mid, len(C), g, "karte") k1, k2 = kenn(r1), kenn(r2) print(zeile(f"gap<={g:.2f} H1", k1)) print(zeile(f"gap<={g:.2f} H2", k2)) print("\n ── URTEIL ─────────────────────────────────────────────────────") a1, a2 = erg["karte"]; c1, c2 = erg["htf"] pruef = [] for nr, txt, ok in ( (1, "OeR>0 und PF>1 in beiden Haelften", bool(a1 and a2 and a1["oer"] > 0 and a2["oer"] > 0 and a1["pf"] > 1 and a2["pf"] > 1)), (2, "KI schliesst die Null in beiden aus", bool(a1 and a2 and a1["u"] is not None and a2["u"] is not None and (a1["u"] > 0 or a1["o"] < 0) and (a2["u"] > 0 or a2["o"] < 0))), (3, "n >= 100 je Haelfte", bool(a1 and a2 and a1["n"] >= 100 and a2["n"] >= 100)), (4, "schlaegt Kontrolle C in beiden", bool(a1 and a2 and c1 and c2 and a1["oer"] > c1["oer"] and a2["oer"] > c2["oer"])), ): pruef.append(ok) print(f" ({nr}) {txt:<42} {'✅' if ok else '❌'}") print(f"\n ==> {'ALLE Bedingungen erfuellt' if all(pruef) else 'REGEL VERFEHLT — nicht bauen'}" f" (Bedingung 5 aus der Nachbar-Tabelle ablesen)") return 0 if __name__ == "__main__": raise SystemExit(main())