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

157 lines
6.4 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
"""Fuellt die ruhende Squeeze-Order wirklich so, wie der Backtest unterstellt?
ANLASS (2026-08-06): Bei `backtest_momentum_v2.py` ist aufgefallen, dass ein
"Einstieg am Level" zwei sehr verschiedene Dinge bedeuten kann:
(a) einsteigen, WO das Signal ausgeloest hat — also dort, wo der Bar-CLOSE das
Level ueberschritten hat, und zwar zum Level-Preis;
(b) einsteigen, WANN die liegende Order gefuellt haette — bei der ERSTEN
Beruehrung des Levels, unabhaengig davon, wo der Bar schliesst.
Live gilt (b). Der Unterschied ist kein Detail: beim Momentum lag er bei **0,45 R**
und hat aus +0,40 ein 0,06 gemacht — die Variante (a) konditioniert auf den
Bar-Schluss, den eine liegende Order nicht kennen kann. Look-ahead.
⚠ DIESELBE FRAGE MUSS FUER DEN SQUEEZE BEANTWORTET WERDEN, denn `squeeze_pending_
entry` laeuft seit 05.08. mit ECHTEM Geld, und die Erwartung dahinter (+0,456/
+0,611 aus `backtest_squeeze_entry.py`) stammt aus Variante (a).
GRUND ZUR HOFFNUNG — aber eben nur Hoffnung, bis gemessen: die Squeeze-Box steht
ueber viele Bars still, das Level ist also ein echter, stabiler Ort fuer eine
Order. Das Momentum-Level wandert dagegen mit `C[iN]` jede Bar mit. Wenn der
Unterschied klein ist, war die Hoffnung berechtigt; ist er gross, ist die
Live-Erwartung zu hoch angesetzt und gehoert korrigiert.
Aufruf: python backtest_squeeze_touchfill.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
_ATRMIN = 0.06
def boxen(H, L, C, A):
"""Je Bar die AKTUELLE Box-Grenze + Ausbruchs-Level — auch ohne Ausbruch.
Das ist der Zustand, in dem die ruhende Order liegt (`armed`): die Box steht,
die beiden Stop-Level stehen. Ob der Kurs sie beruehrt, entscheidet der Markt.
"""
out = []
for i in range(wr._SQ_N + 2, len(C) - 2):
atr = A[i - 1] # am Ende der Vorbar bekannt
if not atr or atr < _ATRMIN:
out.append(None); continue
hb = H[i - wr._SQ_N - 1:i - 1]; lb = L[i - wr._SQ_N - 1:i - 1]
if not hb or not lb:
out.append(None); continue
hi, lo = max(hb), min(lb)
if (hi - lo) / atr > wr._SQ_MULT: # keine Kompression → keine Order
out.append(None); continue
out.append((i, hi + wr._SQ_K * atr, lo - wr._SQ_K * atr, atr))
return out
def lauf_close(H, L, C, SP, A, lo, hi):
"""(a) Projekt-Standard: Einstieg dort, wo `_squeeze_one` `active` meldet
(Bar-CLOSE hat das Level gebrochen), zum Level-Preis."""
rs, frei_ab, last = [], lo, None
for i in range(wr._SQ_N + 2, len(C) - 2):
if not (lo <= i < hi) or i < frei_ab:
continue
atr = A[i]
if not atr or atr < _ATRMIN:
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
d = 1 if st["dir"] == "LONG" else -1
r, xb = simulate(float(st["level"]), d, atr, H, L, C, i + 1, LIVE,
timestop=True, use_tp=True, ret_bar=True)
rs.append(r - (SP[i] if SP[i] > 0 else 0.0225) / atr)
frei_ab = xb + 1
return rs
def lauf_touch(H, L, C, SP, A, B, lo, hi):
"""(b) Realitaet: die Order liegt an beiden Box-Grenzen und fuellt bei der
ERSTEN Beruehrung — auch wenn der Bar wieder in die Box zurueckfaellt."""
rs, frei_ab = [], lo
for eintrag in B:
if eintrag is None:
continue
i, lvl_hi, lvl_lo, atr = eintrag
if not (lo <= i < hi) or i < frei_ab:
continue
d = 0
if H[i] >= lvl_hi:
d, lvl = 1, lvl_hi
elif L[i] <= lvl_lo:
d, lvl = -1, lvl_lo
if not d:
continue
r, xb = simulate(lvl, d, atr, H, L, C, i + 1, LIVE,
timestop=True, use_tp=True, ret_bar=True)
rs.append(r - (SP[i] if SP[i] > 0 else 0.0225) / 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
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)
B = boxen(H, L, C, A)
m = len(C) // 2
print("=" * 92)
print(f" SQUEEZE — Fill bei BAR-CLOSE-Ausbruch gegen Fill bei BERUEHRUNG ({sym})")
print("=" * 92)
print(f" {len(C)} M5-Bars · kanonischer Exit · Echtkosten · sequentiell EIN Slot")
print()
a1 = kennzahlen(lauf_close(H, L, C, SP, A, 0, m))
a2 = kennzahlen(lauf_close(H, L, C, SP, A, m, len(C)))
b1 = kennzahlen(lauf_touch(H, L, C, SP, A, B, 0, m))
b2 = kennzahlen(lauf_touch(H, L, C, SP, A, B, m, len(C)))
for lbl, x1, x2 in (("(a) Close bricht Level (Backtest-Standard)", a1, a2),
("(b) Order fuellt bei Beruehrung (LIVE)", b1, b2)):
if not x1 or not x2:
print(f" {lbl:<44} — keine Trades"); continue
print(f" {lbl:<44} H1 ØR {x1['oer']:+.3f} PF {x1['pf']:.2f} n{x1['n']:<5} | "
f"H2 ØR {x2['oer']:+.3f} PF {x2['pf']:.2f} n{x2['n']:<5}")
if all((a1, a2, b1, b2)):
print()
print(f" Look-ahead-Aufschlag: H1 {a1['oer']-b1['oer']:+.3f} · "
f"H2 {a2['oer']-b2['oer']:+.3f} R je Trade")
print(f" Zusaetzliche Fills durch Beruehrung: H1 {b1['n']-a1['n']:+d} · "
f"H2 {b2['n']-a2['n']:+d}")
gut = b1["oer"] > 0 and b2["oer"] > 0 and b1["pf"] > 1 and b2["pf"] > 1
print()
print(f" → Der Squeeze haelt der realistischen Fill-Annahme "
f"{'STAND ✅' if gut else 'NICHT stand ❌'}")
print(" ⚠ Zum Vergleich Momentum (`backtest_momentum_v2.py`): dort betrug")
print(" der Aufschlag ~0,45 R und drehte +0,40 auf 0,06.")
return 0
if __name__ == "__main__":
sys.exit(main())