Files
AH-Oil-Trader/backtest_momentum_v2.py
T
Axel HocksandClaude Opus 5 45bfc87153 Squeeze auf 9 Instrumenten · Momentum neu gemessen · Fill-Annahme korrigiert · UI
Drei Messungen und eine UI-Aenderung, ausgeloest von der Frage, ob das Konzept
zu ueberdenken ist.

1) SQUEEZE AUF ANDEREN INSTRUMENTEN (backtest_squeeze_multi.py): 7 von 9 halten
   alle vier Bedingungen, alle nachbar-robust. Die zwei Durchfaller (Gasoline,
   USDX) sind exakt die mit Spread >= ATR — der Test scheitert, wo er muss.
   ⚠ 7 sind nicht 7 unabhaengige Tests (Crude/Brent, Gold/Silber, NAS/GER40
   korrelieren), und die Kompression traegt weniger als der Ausbruch selbst
   (Kontrolle "beliebige Box" ist ueberall positiv).

2) MOMENTUM (backtest_momentum_v2.py): bleibt verworfen. Mit Level-Order sah es
   nach +0,4 aus — das war Look-ahead. Eine liegende Order fuellt bei der ersten
   BERUEHRUNG, nicht erst wenn der Bar jenseits des Levels SCHLIESST. Ehrlich
   gerechnet: -0,04..-0,09 in H1. Struktureller Grund: das Momentum-Level wandert
   jede Bar mit C[i-N] und ist damit kein Ort, an dem eine Order liegen bleibt.

3) FILL-ANNAHME (backtest_squeeze_touchfill.py): dieselbe Frage fuer den Squeeze.
   Er haelt (+0,124/+0,292, PF 1,24/1,64) und die Umstellung auf ruhende Orders
   bleibt richtig (Market -0,158/-0,006). ABER die heute zitierten +0,456/+0,611
   sind als Live-Erwartung ~0,32 R zu hoch. Die Beruehrungs-Variante trifft fast
   genau das original validierte Band +0,14..+0,23.
   ⚠ OFFEN: derselbe Test fehlt fuer den SIG-Pending-Pfad, der seit 05.08. live
   ist — und dort ist ein schlechteres Ergebnis zu erwarten, weil das _pend-Level
   wandert statt stillzustehen.

4) UI (v=143): fester Vorbehalt unter der Headline und "Score" statt nackter
   Prozentzahl am Ring. Statisches Markup, kein JS.

Eigener Fehler dokumentiert: der erste Momentum-Entwurf pruefte nicht, ob das
Level als Stop-Order auf der richtigen Marktseite liegt, und lieferte ØR +2,2 —
unmoeglich, und genau daran erkennbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 22:14:02 +02:00

248 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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[iN]` kreuzt ±X×ATR. Aufgeloest nach
dem Preis ist das exakt
Level = C[iN] ± X × ATR
und beide Groessen stehen am Ende von Bar i1 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 i1 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 i1 bekannt (C[iN], A[i1])."""
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[iN] ± X×ATR` WANDERT jede Bar (C[iN]
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[iN] ± X×ATR[i1] (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[iN] — 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())