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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
64eef14871
commit
45bfc87153
@@ -0,0 +1,247 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user