#!/usr/bin/env python3 """P(Trendumkehr) — kalibriertes Modell nach dem Vorbild von P(break). User-Frage 2026-07-31: „können wir die Trendumkehr-Wahrscheinlichkeit berechnen?" WARUM DIESER AUFBAU: Das einzige belegt-prädiktive Modell im Projekt ist P(break) (AUC 0,71 out-of-sample, Kalibrierung trifft). Es gelang, weil das Ereignis **mechanisch scharf** definiert war — ein Barriere-Rennen (Level ± 0,5×ATR binnen 12 Bars), nicht „der Kurs bricht aus". Genau daran scheitern Umkehr-Modelle sonst. EREIGNIS-DEFINITION (dieselbe Bauart): Trendrichtung d = sign(EMA12 − EMA50) mit Totband. Ab Bar i läuft ein Rennen über die nächsten _W Bars: erreicht der Kurs zuerst −_REV×ATR GEGEN d → UMKEHR (y=1) erreicht er zuerst +_REV×ATR MIT d → Fortsetzung (y=0) keins von beidem binnen _W → Fortsetzung (y=0, wie im P(break)-Training beim Timeout) Symmetrisch → die Basisrate sagt direkt, ob der Trend überhaupt persistiert. MERKMALE (alle kausal aus Bars <= i, ATR-normiert): stretch Überdehnung in Trendrichtung (C − EMA50)/ATR × d angle Regressionswinkel × d (dreht der Winkel schon?) mom3/mom6 kurzes/mittleres Momentum × d strength Trendstärke |EMA12−EMA50|/ATR age Bars seit dem letzten EMA-Kreuz (gedeckelt/log-skaliert) gap Abstand zum nächsten M30-Gegenlevel in ATR volr ATR relativ zum eigenen Mittel (Vola-Regime) METHODE: Logistische Regression (Gradientenabstieg auf standardisierten Merkmalen), Fit auf H1, **AUC + Kalibrierung gemessen auf H2**. Ein Modell wird nur dann empfohlen, wenn es out-of-sample trennt UND kalibriert ist — sonst ist es eine weitere unbelegte Zahl (die Lehre aus `analyze_verdict_calibration.py`, wo conf_pct in H2 invertiert war). ⚠ Erwartungsmanagement: Antizyklisch am Extrem ist im Projekt 6× als Münzwurf gemessen, die Bounce-Anzeige hat ~50 % Richtungstreffer. Ein AUC nahe 0,5 wäre also die BESTÄTIGUNG des bisherigen Befunds, kein Rückschlag. """ import sys from bisect import bisect_left from math import exp, log import MetaTrader5 as mt5 _W = 24 # Renn-Fenster in M5-Bars (2 h) _REV = 1.0 # Barriere in ×ATR (symmetrisch) _ATRMIN = 0.06 _DEAD = 0.15 # Totband für die Trendrichtung (wie _TREND_DEADBAND) _ANGLE_LR = 20 _M30 = 6 _PIV_K = 3 _PIV_LOOKBACK = 50 _FEATS = ("stretch", "angle", "mom3", "mom6", "strength", "age", "gap", "volr") def _ema(v, p): k = 2.0 / (p + 1); o = []; e = v[0] for i, x in enumerate(v): e = x if i == 0 else x * k + e * (1 - k) o.append(e) return o def _atr_series(H, L, C, p=14): t = [0.0] for i in range(1, len(C)): t.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1]))) out = [] for i in range(len(C)): if not i: out.append(None); continue w = t[max(1, i - p + 1):i + 1] out.append(sum(w) / max(1, len(w))) return out def _slope(ys): """Steigung einer LS-Regression über ys (x = 0..n-1).""" n = len(ys) if n < 3: return 0.0 mx = (n - 1) / 2.0 my = sum(ys) / n num = sum((i - mx) * (y - my) for i, y in enumerate(ys)) den = sum((i - mx) ** 2 for i in range(n)) return num / den if den else 0.0 def build_levels(H, L): from collections import deque nm = len(H) // _M30 mh = [max(H[i * _M30:(i + 1) * _M30]) for i in range(nm)] ml = [min(L[i * _M30:(i + 1) * _M30]) for i in range(nm)] born = [[] for _ in range(nm)] for p in range(_PIV_K, nm - _PIV_K): if mh[p] == max(mh[p - _PIV_K:p + _PIV_K + 1]) and p + _PIV_K < nm: born[p + _PIV_K].append(mh[p]) if ml[p] == min(ml[p - _PIV_K:p + _PIV_K + 1]) and p + _PIV_K < nm: born[p + _PIV_K].append(ml[p]) lv = []; win = deque(); cur = [] for m in range(nm): win.append(born[m]); cur.extend(born[m]) while len(win) > _PIV_LOOKBACK: for x in win.popleft(): try: cur.remove(x) except ValueError: pass lv.append(sorted(cur)) return lv def samples(O, H, L, C, AT, EF, ES, LV, a, b, step=2): """→ [(feature-dict, y)] — kausal: Merkmale aus <=i, Ausgang aus >i.""" out = [] atr_mean = None age = 0; last_d = 0 # `age` muss lückenlos mitlaufen → über den ganzen Bereich iterieren, # aber nur jeden `step`-ten Bar als Stichprobe nehmen. for i in range(max(a, _ANGLE_LR + 60), min(b, len(C) - _W - 1)): atr = AT[i] if not atr: continue atr = max(atr, _ATRMIN) diff = EF[i] - ES[i] d = 1 if diff > _DEAD * atr else -1 if diff < -_DEAD * atr else 0 if d != last_d and d != 0: age = 0; last_d = d else: age += 1 if d == 0: continue # kein Trend → keine „Umkehr" definierbar if (i - a) % step: continue # ── Ausgang: Barriere-Rennen up = C[i] + _REV * atr dn = C[i] - _REV * atr hit_rev = hit_con = None for j in range(i + 1, min(i + 1 + _W, len(C))): if d > 0: if L[j] <= dn: hit_rev = j; break if H[j] >= up: hit_con = j; break else: if H[j] >= up: hit_rev = j; break if L[j] <= dn: hit_con = j; break y = 1 if hit_rev is not None else 0 # ── Merkmale w = C[max(0, i - _ANGLE_LR):i + 1] ang = _slope(w) / atr * _ANGLE_LR # normierte Steigung atr_w = [x for x in AT[max(1, i - 200):i + 1] if x] atr_mean = (sum(atr_w) / len(atr_w)) if atr_w else atr lv = LV[min(i // _M30, len(LV) - 1)] gap = 3.0 if lv: k = bisect_left(lv, C[i]) # Gegenlevel = das, auf das der Trend zuläuft tgt = None if d > 0 and k < len(lv): tgt = lv[k] elif d < 0 and k > 0: tgt = lv[k - 1] if tgt is not None: gap = min(3.0, abs(tgt - C[i]) / atr) f = { "stretch": (C[i] - ES[i]) / atr * d, "angle": ang * d, "mom3": (C[i] - C[i - 3]) / atr * d, "mom6": (C[i] - C[i - 6]) / atr * d, "strength": abs(diff) / atr, "age": log(1 + min(age, 300)), "gap": gap, "volr": atr / atr_mean if atr_mean else 1.0, } out.append((f, y)) return out def fit(rows, iters=400, lr=0.35): """Logistische Regression auf standardisierten Merkmalen → (mu, sd, w).""" n = len(rows) mu = {}; sd = {} for k in _FEATS: xs = [r[0][k] for r in rows] m = sum(xs) / n v = sum((x - m) ** 2 for x in xs) / n mu[k] = m; sd[k] = (v ** 0.5) or 1.0 X = [[(r[0][k] - mu[k]) / sd[k] for k in _FEATS] for r in rows] Y = [r[1] for r in rows] w = [0.0] * (len(_FEATS) + 1) # w[0] = Bias for _ in range(iters): g = [0.0] * len(w) for xi, yi in zip(X, Y): z = w[0] + sum(wk * xk for wk, xk in zip(w[1:], xi)) p = 1.0 / (1.0 + exp(-max(-30, min(30, z)))) e = p - yi g[0] += e for t in range(len(xi)): g[t + 1] += e * xi[t] for t in range(len(w)): w[t] -= lr * g[t] / n return mu, sd, w def predict(f, mu, sd, w): z = w[0] + sum(w[t + 1] * ((f[k] - mu[k]) / sd[k]) for t, k in enumerate(_FEATS)) return 1.0 / (1.0 + exp(-max(-30, min(30, z)))) def auc(ps, ys): pairs = sorted(zip(ps, ys)) pos = sum(ys); neg = len(ys) - pos if not pos or not neg: return 0.5 rank = 0.0; i = 0 while i < len(pairs): j = i while j + 1 < len(pairs) and pairs[j + 1][0] == pairs[i][0]: j += 1 r = (i + j) / 2.0 + 1 for k in range(i, j + 1): if pairs[k][1] == 1: rank += r i = j + 1 return (rank - pos * (pos + 1) / 2) / (pos * neg) def main(): global _REV, _W n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000 # Ereignis-Definition variabel — ein Null-Ergebnis ist nur dann belastbar, wenn # es nicht an EINER willkürlichen Schwelle hängt. if len(sys.argv) > 2: _REV = float(sys.argv[2]) if len(sys.argv) > 3: _W = int(sys.argv[3]) mt5.initialize(); sym = None for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD"): if mt5.symbol_info(c): sym = c; break bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n) mt5.shutdown() O = [float(b["open"]) for b in bars]; H = [float(b["high"]) for b in bars] L = [float(b["low"]) for b in bars]; C = [float(b["close"]) for b in bars] AT = _atr_series(H, L, C); EF = _ema(C, 12); ES = _ema(C, 50) LV = build_levels(H, L) N = len(C); mid = N // 2 print("=" * 96) print(f" P(TRENDUMKEHR) — {sym} M5 ({N} Bars)") print(f" Ereignis: erreicht der Kurs zuerst −{_REV}×ATR GEGEN den Trend (Umkehr)") print(f" oder +{_REV}×ATR MIT dem Trend, binnen {_W} Bars = {_W*5} min?") print(" Fit auf H1, AUC + Kalibrierung gemessen auf H2 (out-of-sample).") print("=" * 96) tr = samples(O, H, L, C, AT, EF, ES, LV, 100, mid) te = samples(O, H, L, C, AT, EF, ES, LV, mid, N) if not tr or not te: print("zu wenig Daten"); return br_tr = sum(y for _, y in tr) / len(tr) br_te = sum(y for _, y in te) / len(te) print(f"\n Stichprobe H1 n={len(tr)} Basisrate Umkehr {100*br_tr:.1f}%") print(f" H2 n={len(te)} Basisrate Umkehr {100*br_te:.1f}%") print(" (50 % = der Trend persistiert gar nicht; <50 % = Trendfolge trägt)") mu, sd, w = fit(tr) print("\n Gewichte (standardisiert, + = erhöht P(Umkehr)):") print(f" {'bias':<10}{w[0]:+.3f}") for t, k in enumerate(_FEATS): print(f" {k:<10}{w[t+1]:+.3f}") ps = [predict(f, mu, sd, w) for f, _ in te] ys = [y for _, y in te] a = auc(ps, ys) print(f"\n AUC out-of-sample (H2): {a:.3f} " f"(0,50 = Münzwurf · P(break) erreichte 0,71)") print("\n Kalibrierung H2 — trifft die vorhergesagte Rate die echte?") print(f" {'Bucket':<14}{'n':>7}{'vorhergesagt':>14}{'real':>9}") edges = [0.0, 0.35, 0.45, 0.50, 0.55, 0.65, 1.01] for lo, hi in zip(edges, edges[1:]): sel = [(p, y) for p, y in zip(ps, ys) if lo <= p < hi] if len(sel) < 50: continue pm = sum(p for p, _ in sel) / len(sel) rm = sum(y for _, y in sel) / len(sel) print(f" {lo:.2f}–{hi:<9.2f}{len(sel):>7}{100*pm:>13.1f}%{100*rm:>8.1f}%") # In-sample-Kontrolle: trennt es wenigstens auf den Trainingsdaten? ps_tr = [predict(f, mu, sd, w) for f, _ in tr] print(f"\n (Kontrolle) AUC in-sample H1: {auc(ps_tr, [y for _, y in tr]):.3f}") print("\n Einzelmerkmale — AUC allein (H2), zeigt wer überhaupt trennt:") for k in _FEATS: xs = [f[k] for f, _ in te] ak = auc(xs, ys) print(f" {k:<10}{ak:.3f}" + (" <-- traegt" if abs(ak - 0.5) >= 0.03 else "")) if __name__ == "__main__": main()