#!/usr/bin/env python3 """Momentum-Continuation — neu gemessen mit ruhender Order am Trigger-Level. ANLASS (2026-08-06, User: „backteste auch das Momentum-Signal"). ⚠ WARUM DAS KEINE WIEDERHOLUNG IST. `backtest_momentum.py` (2026-07-17) hat die Idee verworfen: fast alle N/X in H1 negativ, in H2 positiv → regime-abhaengig. Dieser Lauf steigt **am Bar-Close** ein. Genau diese Annahme ist heute zweimal gekippt — beim Squeeze (−0,158 → +0,456) und beim Auto-Signal (−0,09 → +0,35), allein durch den Einstieg AM LEVEL statt am Markt. Momentum hat ein sauber definiertes Level, also ist die Frage offen. DAS LEVEL: der Trigger ist `mom_N = C[i] − C[i−N]` kreuzt ±X×ATR. Aufgeloest nach dem Preis ist das exakt Level = C[i−N] ± X × ATR und beide Groessen stehen am Ende von Bar i−1 fest. Eine BUY_STOP/SELL_STOP-Order dort ist also **kausal platzierbar** — kein Look-ahead. Gefuellt wird sie, wenn die Bar-Spanne das Level beruehrt; der Exit startet erst auf der Folgebar. ⚠ Bewusst `A[i-1]`, nicht `A[i]`: der ATR von Bar i ist am Ende von i−1 noch nicht bekannt. Der alte Lauf durfte das benutzen, die ruhende Order darf es nicht. ⚠⚠ GEGENHYPOTHESE, die dieser Lauf ebenfalls prueft. Aus dem Kerzen-Fade von heute stammt die Regel: **ruhende Order hilft bei MOMENTUM-Einstiegen, schadet bei REVERSION-Einstiegen** (dort fuellt sie nur, wenn man falsch liegt). Momentum- Continuation ist ein Momentum-Einstieg — die Regel sagt also „hilft". Faellt es trotzdem durch, ist die Regel selbst angezaehlt. Deshalb laufen A und B parallel. VORAB FIXIERTE ENTSCHEIDUNGSREGEL: (1) ØR > 0 UND PF > 1 in BEIDEN Haelften (2) mind. 40 Trades je Haelfte (3) die Nachbar-Parameter (N und X je eine Stufe) kippen das Vorzeichen nicht Sonst: bleibt verworfen. Kein nachtraegliches Lockern. Aufruf: python backtest_momentum_v2.py [bars] """ import sys import MetaTrader5 as mt5 from backtest_cost_gate import _atr_series, kennzahlen from core.exit_model import LIVE, simulate _N = (12, 24, 48) # Momentum-Fenster in M5-Bars (1 h / 2 h / 4 h) _X = (1.0, 1.5, 2.0) # Schwelle in ×ATR _ATRMIN = 0.06 _MIN_N = 40 def signale(H, L, C, SP, A, N, X): """Trigger-Kandidaten. `lvl` ist am Ende von Bar i−1 bekannt (C[i−N], A[i−1]).""" out, letzte = [], None for i in range(max(N, 20) + 1, len(C) - 2): atr = A[i - 1] if not atr or atr < _ATRMIN: continue mom_v, mom = C[i - 1] - C[i - 1 - N], C[i] - C[i - N] d = 0 if mom_v <= X * atr < mom: d = 1 elif mom_v >= -X * atr > mom: d = -1 if not d: continue lvl = C[i - N] + d * X * atr if letzte == (d, round(lvl, 3)): continue letzte = (d, round(lvl, 3)) out.append({"i": i, "d": d, "lvl": lvl, "atr": atr, "kosten": (SP[i] if SP[i] > 0 else 0.0225) / atr}) return out def lauf(K, H, L, C, lo, hi, am_level): """Sequentiell, EIN Slot. `am_level`=True → ruhende Stop-Order, sonst Bar-Close.""" rs, frei_ab = [], lo for k in K: i = k["i"] if not (lo <= i < hi) or i < frei_ab: continue d, atr, lvl = k["d"], k["atr"], k["lvl"] if am_level: # ⚠⚠ PLATZIERBARKEIT: ein BUY_STOP muss UEBER dem Markt liegen, ein # SELL_STOP darunter. Fehlt diese Pruefung, "fuellt" die Sim zu einem # Preis, den der Markt laengst hinter sich hat — das ist keine Order, # das ist geschenktes Geld. Genau daran ist der erste Entwurf # gescheitert (ØR +2,2 in Variante C). if (lvl <= C[i - 1]) if d > 0 else (lvl >= C[i - 1]): continue # Gefuellt nur, wenn die Bar-Spanne das Level erreicht. if (H[i] < lvl) if d > 0 else (L[i] > lvl): continue entry = lvl else: entry = C[i] r, xb = simulate(entry, d, atr, H, L, C, i + 1, LIVE, timestop=True, use_tp=True, ret_bar=True) rs.append(r - k["kosten"]) frei_ab = xb + 1 return rs def lauf_echt(H, L, C, SP, A, N, X, lo, hi): """⚠⚠ DIE EHRLICHE VARIANTE — und sie widerlegt Variante B. In B wird nur dort eingestiegen, wo das Momentum am Bar-CLOSE gekreuzt hat. Eine real liegende Stop-Order weiss davon nichts: sie fuellt bei der ERSTEN Beruehrung — auch bei allen Beruehrungen, die wieder zurueckfallen und nie ein Signal werden. B konditioniert also auf den Bar-Schluss und ist damit Look-ahead. Verschaerfend: das Momentum-Level `C[i−N] ± X×ATR` WANDERT jede Bar (C[i−N] rutscht mit). Anders als die Squeeze-Box, die ueber viele Bars stillsteht, ist das kein Ort, an dem eine Order liegen bleiben koennte — sie muesste im 5-Minuten-Takt umgesetzt werden und faengt dabei jede Zuckung ein. Hier deshalb: an JEDER Bar beide Stop-Orders am jeweils gueltigen Level, Fill bei Beruehrung, Exit ab der Folgebar. Das ist das, was live passieren wuerde.""" rs, frei_ab = [], lo for j in range(max(N, 20) + 1, min(hi, len(C) - 2)): if j < frei_ab or j < lo: continue atr = A[j - 1] if not atr or atr < _ATRMIN: continue basis = C[j - N] for d in (1, -1): lvl = basis + d * X * atr # ⚠ nur platzierbare Stop-Level (s. Kommentar in `lauf`) if (lvl <= C[j - 1]) if d > 0 else (lvl >= C[j - 1]): continue if (H[j] < lvl) if d > 0 else (L[j] > lvl): continue r, xb = simulate(lvl, d, atr, H, L, C, j + 1, LIVE, timestop=True, use_tp=True, ret_bar=True) rs.append(r - (SP[j] if SP[j] > 0 else 0.0225) / atr) frei_ab = xb + 1 break 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] SP = [float(x["spread"]) * point for x in b] A = _atr_series(H, L, C) m = len(C) // 2 print("=" * 98) print(f" MOMENTUM-CONTINUATION — Bar-Close gegen ruhende Order am Trigger-Level") print("=" * 98) print(f" {len(C)} M5-Bars · kanonischer Exit · Echtkosten · sequentiell EIN Slot") print(" Level = C[i−N] ± X×ATR[i−1] (am Ende der Vorbar bekannt = platzierbar)") print() print(f" {'N':>3} {'X':>4} | {'A) Bar-Close H1 / H2':^30} | " f"{'B) am Level H1 / H2':^30} | Urteil") print(" " + "-" * 94) best = [] for N in _N: for X in _X: K = signale(H, L, C, SP, A, N, X) a1, a2 = (kennzahlen(lauf(K, H, L, C, 0, m, False)), kennzahlen(lauf(K, H, L, C, m, len(C), False))) b1, b2 = (kennzahlen(lauf(K, H, L, C, 0, m, True)), kennzahlen(lauf(K, H, L, C, m, len(C), True))) if not all((a1, a2, b1, b2)): print(f" {N:>3} {X:>4} | zu wenige Signale") continue ok = (b1["oer"] > 0 and b2["oer"] > 0 and b1["pf"] > 1 and b2["pf"] > 1 and b1["n"] >= _MIN_N and b2["n"] >= _MIN_N) print(f" {N:>3} {X:>4} | {a1['oer']:>+7.3f} n{a1['n']:<4} / " f"{a2['oer']:>+7.3f} n{a2['n']:<4} | " f"{b1['oer']:>+7.3f} n{b1['n']:<4} / " f"{b2['oer']:>+7.3f} n{b2['n']:<4} | {'✅' if ok else '❌'}") if ok: best.append((min(b1["oer"], b2["oer"]), N, X)) print() print("=" * 98) if not best: print(" ERGEBNIS: keine Kombination besteht — Momentum bleibt VERWORFEN.") print(" ⚠ Das ist zugleich ein Test der heutigen Regel „ruhende Order hilft") print(" bei Momentum-Einstiegen — haelt sie hier nicht, ist sie enger zu") print(" fassen als gedacht (dann gilt sie fuer AUSBRUECHE, nicht fuer") print(" Momentum allgemein).") return 0 print() print("=" * 98) print(" ⚠⚠ GEGENPROBE — ist so eine Order ueberhaupt platzierbar?") print("=" * 98) print(" B steigt nur ein, wo das Momentum am Bar-CLOSE gekreuzt hat. Eine real") print(" liegende Order weiss das nicht: sie fuellt bei der ERSTEN Beruehrung,") print(" auch bei denen, die zurueckfallen. Und das Level wandert jede Bar mit") print(" C[i−N] — anders als die Squeeze-Box, die stillsteht.") print() print(f" {'N':>3} {'X':>4} | {'B) nur bei Kreuzung':^26} | " f"{'C) jede Beruehrung (echt)':^28}") print(" " + "-" * 70) for N in _N: for X in _X: K = signale(H, L, C, SP, A, N, X) b1, b2 = (kennzahlen(lauf(K, H, L, C, 0, m, True)), kennzahlen(lauf(K, H, L, C, m, len(C), True))) c1, c2 = (kennzahlen(lauf_echt(H, L, C, SP, A, N, X, 0, m)), kennzahlen(lauf_echt(H, L, C, SP, A, N, X, m, len(C)))) if not all((b1, b2, c1, c2)): continue gut = c1["oer"] > 0 and c2["oer"] > 0 print(f" {N:>3} {X:>4} | {b1['oer']:>+7.3f} / {b2['oer']:>+7.3f} n{b1['n']:<5} | " f"{c1['oer']:>+7.3f} / {c2['oer']:>+7.3f} n{c1['n']:<5} " f"{'✅' if gut else '❌'}") best.sort(reverse=True) _, N, X = best[0] print(f" BESTE KOMBINATION: N={N} · X={X} → Nachbar-Pruefung") print("=" * 98) alle = True for nn in sorted({max(6, N - 12), N, N + 12}): for xx in sorted({round(X - 0.5, 1), X, round(X + 0.5, 1)}): if xx <= 0: continue K = signale(H, L, C, SP, A, nn, xx) q1, q2 = (kennzahlen(lauf(K, H, L, C, 0, m, True)), kennzahlen(lauf(K, H, L, C, m, len(C), True))) if not q1 or not q2: continue gut = q1["oer"] > 0 and q2["oer"] > 0 alle &= gut mark = " ← Optimum" if (nn == N and xx == X) else "" print(f" N={nn:>3} X={xx:<4} H1 {q1['oer']:>+7.3f} n{q1['n']:<4} | " f"H2 {q2['oer']:>+7.3f} n{q2['n']:<4} {'✅' if gut else '❌'}{mark}") print() print(f" → {'ROBUST — Kandidat' if alle else 'KIPPT bei den Nachbarn — verworfen'}") return 0 if __name__ == "__main__": sys.exit(main())