Files
AH-Oil-Trader/backtest_squeeze_multi.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

269 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
"""Traegt der validierte Squeeze auch auf ANDEREN Instrumenten?
ANLASS (2026-08-06): `analyze_signal_live.py` hat gezeigt, dass die Richtungs-
aussage der Empfehlung ein Muenzwurf ist (4851 % ueber 1.071 Episoden). Der
Ertrag kommt aus Einstiegspreis und Exit-Asymmetrie. Damit ist das Grundproblem
nicht die QUALITAET des Squeeze, sondern seine **Frequenz** — 23×/Woche auf
einem Instrument.
Darauf hat das Projekt bisher zweimal reagiert: mehr Signale auf demselben
Instrument suchen (**23 verworfene Eingriffe**) oder auf eine feinere Zeitebene
gehen (`backtest_squeeze_m1.py` — die Kosten erledigen es). Die dritte Achse ist
unberuehrt: **dasselbe Signal, mehr Instrumente.** Praezedenzfall: `breakout_k=0,3`
wurde 2026-07-09 auf Brent nachvalidiert (ØR +0,305) — die Mechanik
Kompression→Ausbruch ist nicht oelspezifisch.
⚠⚠ ZWEI FALLEN, DIE HIER EINGEBAUT SIND — beide sind im Projekt schon einmal
teuer gewesen:
1) **Der ATR-Floor 0,06 ist ein WTI-DOLLAR-Wert.** Auf Gold (ATR ~3) waere er
wirkungslos, auf NatGas (~0,05) wuerde er fast alles wegwerfen — und ein
Filter, der nach einer Eigenschaft aussortiert, die mit der untersuchten
Achse korreliert, ERZEUGT den Befund (Lehre aus `analyze_tageszeit.py` und
dem ORB-Selektions-Artefakt). Der Floor wird deshalb **relativ** gesetzt:
derselbe Anteil am Median-ATR wie bei WTI.
2) **MEHRFACHVERGLEICH.** Acht Instrumente = acht Chancen auf einen Zufalls-
treffer. Bei 5 % Fehlalarmquote ist ~0,4 davon zu erwarten. Deshalb gilt die
Regel unten UND es wird ausgewiesen, wie viele Kandidaten bestanden haben —
bestehen 1 von 8, ist das etwas anderes als 6 von 8.
VORAB FIXIERTE ENTSCHEIDUNGSREGEL (vor dem ersten Lauf festgelegt):
Ein Instrument gilt nur dann als Kandidat, wenn
(1) ØR > 0 UND PF > 1 in BEIDEN Haelften,
(2) mindestens 40 Ausbrueche je Haelfte (sonst Rauschen),
(3) die Nachbar-Parameter (Box 10/15 × k 0,1/0,2) das Vorzeichen nicht kippen,
(4) SpotCrude als KONTROLLE seine bekannten Werte reproduziert — sonst ist die
Pipeline defekt und nichts davon zaehlt.
Aufruf: python backtest_squeeze_multi.py [bars]
"""
import sys
import MetaTrader5 as mt5
import core.wave_rec as wr
from backtest_cost_gate import _atr_series, kennzahlen
from core.exit_model import LIVE, simulate
from core.wave_rec import WaveRecommender
_SYMBOLE = ["SpotCrude", # Kontrolle — muss reproduzieren
"SpotBrent", "NatGas", "Gasoline", "Copper",
"XAUUSD", "XAGUSD", "NAS100", "GER40", "USDX"]
_MIN_N = 40
def hole(sym, n):
if not mt5.symbol_select(sym, True):
return None
si = mt5.symbol_info(sym)
b = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n)
if b is None or len(b) < 5000 or si is None:
return None
return {"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"]) * si.point for x in b], "digits": si.digits}
def kandidaten(d, A, floor, box=None, k=None, mult=None):
"""`mult` = Kompressions-Schwelle. Sehr hoch gesetzt (99) entfaellt die
Kompressions-Bedingung → Ausbruch aus einer BELIEBIGEN Box = die Kontrolle,
die den Squeeze auf WTI ueberhaupt erst validiert hat (dort schlug er die
beliebige Box in BEIDEN Haelften: +0,227/+0,141 gegen 0,045/+0,161)."""
alt_n, alt_k, alt_m = wr._SQ_N, wr._SQ_K, wr._SQ_MULT
if box is not None:
wr._SQ_N = box
if k is not None:
wr._SQ_K = k
if mult is not None:
wr._SQ_MULT = mult
H, L, C, SP = d["H"], d["L"], d["C"], d["SP"]
out, last = [], None
try:
for i in range(wr._SQ_N + 2, len(C) - 2):
atr = A[i]
if not atr or atr < floor:
continue
w0 = i - wr._SQ_N - 2
st = WaveRecommender._squeeze_one(H[w0:i + 1], L[w0:i + 1],
C[w0:i + 1], atr)
if st.get("state") != "active":
last = None
continue
sig = (st["dir"], st["level"])
if sig == last:
continue
last = sig
# ⚠ Kosten: kein WTI-Ersatzwert (0,0225) fuer fremde Instrumente —
# fehlt der Bar-Spread, wird der Median dieses Symbols genommen.
out.append({"i": i, "d": 1 if st["dir"] == "LONG" else -1,
"lvl": float(st["level"]), "atr": atr, "sp": SP[i]})
finally:
wr._SQ_N, wr._SQ_K, wr._SQ_MULT = alt_n, alt_k, alt_m
return out
def lauf(K, d, lo, hi, med_sp, slip=0.0):
H, L, C = d["H"], d["L"], d["C"]
rs, frei_ab = [], lo
for kd in K:
i = kd["i"]
if not (lo <= i < hi) or i < frei_ab:
continue
sp = kd["sp"] if kd["sp"] > 0 else med_sp
entry = kd["lvl"] + kd["d"] * slip * kd["atr"]
r, xb = simulate(entry, kd["d"], kd["atr"], H, L, C, i + 1, LIVE,
timestop=True, use_tp=True, ret_bar=True)
rs.append(r - sp / kd["atr"])
frei_ab = xb + 1
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
daten, atr_med = {}, {}
for s in _SYMBOLE:
d = hole(s, n)
if not d:
print(f" ⚠ {s}: keine Daten / nicht handelbar — uebersprungen")
continue
d["A"] = _atr_series(d["H"], d["L"], d["C"])
gute = sorted(a for a in d["A"][20:] if a > 0)
if not gute:
continue
atr_med[s] = gute[len(gute) // 2]
sps = sorted(x for x in d["SP"][20:] if x > 0)
d["med_sp"] = sps[len(sps) // 2] if sps else 0.0
daten[s] = d
mt5.shutdown()
if "SpotCrude" not in daten:
print("SpotCrude fehlt — ohne Kontrolle keine Aussage"); return 1
# Relativer Floor: derselbe Anteil am Median-ATR wie live bei WTI
rel = 0.06 / atr_med["SpotCrude"]
print("=" * 100)
print(" SQUEEZE AUF MEHREREN INSTRUMENTEN — dasselbe Signal, mehr Maerkte")
print("=" * 100)
print(f" Bars je Symbol: {n} (M5) · Einstieg AM Level (ruhende Stop-Order) ·")
print(f" kanonischer Exit · Echtkosten aus dem Bar-Spread · sequentiell EIN Slot")
print(f" ATR-Floor relativ: {rel:.3f} × Median-ATR "
f"(= 0,06 $ bei WTI, massstabsgerecht uebertragen)")
print()
print(f" {'Symbol':<12} {'Med ATR':>9} {'Med Spr':>9} {'Spr/ATR':>8} "
f"{'H1 ØR':>8} {'PF':>5} {'n':>5} | {'H2 ØR':>8} {'PF':>5} {'n':>5} Urteil")
print(" " + "-" * 96)
besteht = []
for s, d in daten.items():
floor = rel * atr_med[s]
K = kandidaten(d, d["A"], floor)
m = len(d["C"]) // 2
r1 = lauf(K, d, 0, m, d["med_sp"])
r2 = lauf(K, d, m, len(d["C"]), d["med_sp"])
k1, k2 = kennzahlen(r1), kennzahlen(r2)
if not k1 or not k2:
print(f" {s:<12} — keine Ausbrueche")
continue
ok = (k1["oer"] > 0 and k2["oer"] > 0 and k1["pf"] > 1 and k2["pf"] > 1
and k1["n"] >= _MIN_N and k2["n"] >= _MIN_N)
duenn = "" if (k1["n"] >= _MIN_N and k2["n"] >= _MIN_N) else " (zu duenn)"
print(f" {s:<12} {atr_med[s]:>9.4f} {d['med_sp']:>9.4f} "
f"{d['med_sp']/atr_med[s]:>8.3f} "
f"{k1['oer']:>+8.3f} {k1['pf']:>5.2f} {k1['n']:>5} | "
f"{k2['oer']:>+8.3f} {k2['pf']:>5.2f} {k2['n']:>5} "
f"{'✅' if ok else '❌'}{duenn}")
if ok and s != "SpotCrude":
besteht.append(s)
print()
print("=" * 100)
print(" NACHBAR-PARAMETER der bestandenen Kandidaten (Box × k)")
print("=" * 100)
if not besteht:
print(" keiner bestanden — nichts zu pruefen")
fest = []
for s in besteht:
d = daten[s]
floor = rel * atr_med[s]
m = len(d["C"]) // 2
alle_ok, zeilen = True, []
for box, kk in ((10, 0.1), (15, 0.1), (10, 0.2), (15, 0.2)):
K = kandidaten(d, d["A"], floor, box=box, k=kk)
a, b = (kennzahlen(lauf(K, d, 0, m, d["med_sp"])),
kennzahlen(lauf(K, d, m, len(d["C"]), d["med_sp"])))
if not a or not b:
alle_ok = False
zeilen.append(f" Box {box} k {kk}: keine Ausbrueche")
continue
gut = a["oer"] > 0 and b["oer"] > 0
alle_ok &= gut
zeilen.append(f" Box {box} k {kk}: H1 {a['oer']:+.3f} (n{a['n']}) | "
f"H2 {b['oer']:+.3f} (n{b['n']}) {'✅' if gut else '❌'}")
print(f" {s}:")
for z in zeilen:
print(z)
print(f" → {'ROBUST' if alle_ok else 'KIPPT — verworfen'}")
if alle_ok:
fest.append(s)
print()
print("=" * 100)
print(" KONTROLLE — schlaegt die KOMPRESSION den Ausbruch aus BELIEBIGER Box?")
print("=" * 100)
print(" ⚠ Ohne diese Zeile waere der Befund nicht interpretierbar: 'Breakout +")
print(" Trailing' hat generisch Edge. Traegt die Kompression nichts bei, misst")
print(" man nur den Exit — und dann braucht es kein Squeeze-Modul.")
print(f" {'Symbol':<12} {'Squeeze H1/H2':>22} {'beliebige Box H1/H2':>24} Beitrag")
for s in fest:
d = daten[s]; floor = rel * atr_med[s]; m = len(d["C"]) // 2
Ks = kandidaten(d, d["A"], floor)
Kb = kandidaten(d, d["A"], floor, mult=99.0)
a1, a2 = (kennzahlen(lauf(Ks, d, 0, m, d["med_sp"])),
kennzahlen(lauf(Ks, d, m, len(d["C"]), d["med_sp"])))
b1, b2 = (kennzahlen(lauf(Kb, d, 0, m, d["med_sp"])),
kennzahlen(lauf(Kb, d, m, len(d["C"]), d["med_sp"])))
if not all((a1, a2, b1, b2)):
continue
gut = a1["oer"] > b1["oer"] and a2["oer"] > b2["oer"]
print(f" {s:<12} {a1['oer']:>+10.3f}/{a2['oer']:>+10.3f} "
f"{b1['oer']:>+12.3f}/{b2['oer']:>+10.3f} "
f"{'✅ Kompression traegt' if gut else '❌ kein Beitrag'}")
print()
print("=" * 100)
print(" SLIPPAGE-HAERTETEST — die Stop-Order loest aus, fuellt aber zum Markt")
print("=" * 100)
for s in fest:
d = daten[s]; floor = rel * atr_med[s]; m = len(d["C"]) // 2
K = kandidaten(d, d["A"], floor)
zeile = f" {s:<12}"
for sl in (0.0, 0.10, 0.20):
x1 = kennzahlen(lauf(K, d, 0, m, d["med_sp"], slip=sl))
x2 = kennzahlen(lauf(K, d, m, len(d["C"]), d["med_sp"], slip=sl))
if x1 and x2:
zeile += f" +{sl:.2f}: {x1['oer']:+.3f}/{x2['oer']:+.3f}"
print(zeile)
print()
print("=" * 100)
print(f" ERGEBNIS: {len(fest)} von {len(daten)-1} geprueften Fremd-Instrumenten "
f"halten alle vier Bedingungen")
if fest:
print(f" Kandidaten: {', '.join(fest)}")
print()
print(" ⚠ MEHRFACHVERGLEICH mitlesen: bei 5 % Fehlalarmquote und "
f"{len(daten)-1} Tests")
print(" waere ~0,4 Zufallstreffer zu erwarten. 1 von 9 ist deshalb schwach,")
print(" mehrere sind ein Muster. Die Nachbar-Pruefung senkt die Rate zusaetzlich.")
print(" ⚠ NICHT gemessen: Korrelation der Instrumente untereinander (mehrere")
print(" Slots gleichzeitig = evtl. dasselbe Risiko doppelt), Swap/Kommission")
print(" je Symbol, Handelszeiten und Mindest-Lots. Das gehoert VOR einen Bau.")
return 0
if __name__ == "__main__":
sys.exit(main())