Files
AH-Oil-Trader/backtest_squeeze_frozen.py
Axel HocksandClaude Opus 5 d87bda1461 FIX: setup-Nachtrag bei Pending-Fill scheiterte an einem Rennen
User: "dann sieh dir die Luecke an" (nach "DB-Nachtrag fehlgeschlagen" im Log,
20.08. 07:40:12, T=50371599).

URSACHE - ein Rennen zwischen zwei Pfaden: _check_pending_fill erkennt den Fill
aus trader_brk.snapshot(), die DB-ZEILE schreibt aber trader._refresh_locked ->
log_trade_open. War der noch nicht durch, traf das UPDATE keine Zeile und der
Nachtrag scheiterte STILL. Der Trade blieb setup=NULL und war damit von einem
manuellen nicht unterscheidbar - genau die Buchfuehrungs-Luecke, gegen die
tag_bot_trade ueberhaupt gebaut wurde (Deployment-Drift Fall 8), nur eine Ebene
tiefer. Folge: squeeze_entry_count blieb 0, der Trade fehlte in B4/B5.

BEHOBEN: gescheiterte Nachtraege landen in `_tag_nachtrag` und werden im
_pos_loop wiederholt, bis die Zeile da ist - dasselbe Retry-Muster wie beim
Gewinn-Close (23.07.). Nach 60 Versuchen (~1 min) wird aufgegeben und LAUT
gemeldet: eine Zeile, die nach einer Minute nicht existiert, kommt nicht mehr,
und dann soll es nicht still bleiben.

T=50371599 nachgetragen (DB-Backup oil_widget_history.db.bak-2026-08-20).

 NEBENBEFUND, und zwar ein guter: derselbe Logausschnitt enthaelt
"🧷 BRK-Slot uebernimmt Pending-Fill T=50371599" - das ist der ERSTE
Live-Beleg, dass der Zwei-Slot-Umbau vom 19.08. funktioniert. Der BRK-Slot hat
die gefuellte Pending-Order korrekt uebernommen.

⚠ ZWEITER NEBENBEFUND, NICHT behoben (User-Entscheidung): die Order wurde mit
0,01 Lots platziert. Das ist die direkte Folge von margin_brk = 10 % auf eine
fast erschoepfte freie Margin - die Einstellung wirkt also, nur vermutlich
anders als gedacht. Dazu steht margin_pct in runtime_state auf 95, waehrend die
ini 40 sagt (runtime gewinnt). Beides gemeldet, nichts geaendert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 08:12:15 +02:00

159 lines
6.8 KiB
Python

"""Wandert die Squeeze-Marke dem Kurs entgegen — und was kostet das? (2026-08-20)
ANLASS (User, live beobachtet): "warum hat der breakout squeeze ausgeloest, ich
kann keinen breakout erkennen". Die Logspur vom 20.08. gibt ihm recht:
07:25:08 BUY_STOP @ 85.457
07:30:01 BUY_STOP @ 85.432
07:35:04 BUY_STOP @ 85.417
07:40:03 BUY_STOP @ 85.342 <- 11,5 Cent tiefer als 15 min zuvor
07:40:12 FILL @ 85.344 <- 9 Sekunden spaeter
Der Kurs stieg in dem Fenster von 85,176 auf 85,355 - er hat die Marke NICHT
durchbrochen, die MARKE kam ihm entgegen.
URSACHE. `box_hi` ist das Hoch der letzten `_SQ_N` abgeschlossenen M5-Bars, also
ein ROLLIERENDES Fenster. Faellt der Kurs, rollen die alten hohen Bars hinten
heraus und das Box-Dach sinkt mit; die ruhende Order wird nachgezogen.
⚠⚠ DAS VERLETZT DAS PRINZIP, AUF DEM DER PENDING-UMBAU VOM 05.08. BERUHT und das
im Projekt woertlich steht: "Ein Level ist nur dann ein Ausfuehrungs-Vorteil,
wenn es STILLSTEHT." Genau daran ist der SIG-Pending-Pfad gescheitert
(OeR -0,392/-0,338, abgeschaltet) - `_pend` wandert, und die Order faengt
systematisch die Fehlausbrueche ein. Der Squeeze galt als der Fall, wo das Level
STEHT: `backtest_squeeze_touchfill.py` misst den Ausbruch aus einer Box, die im
Moment des Ausbruchs feststeht. Live gleitet sie.
WAS HIER GEMESSEN WIRD - zwei Varianten auf DENSELBEN Bars:
A) NACHGEZOGEN (= live): die Marke wird jede Bar neu aus der aktuellen Box
berechnet; gefuellt wird, sobald der Kurs die JEWEILS AKTUELLE Marke
beruehrt. Bildet das beobachtete Verhalten nach.
B) EINGEFROREN: die Marke wird EINMAL gesetzt, wenn die Kompression beginnt,
und bleibt stehen, bis die Kompression endet. Gefuellt wird nur bei
Beruehrung DIESER Marke. Das ist, was der Backtest unterstellt.
Zusaetzlich wird in A ausgezaehlt, wie viele Fills reine NACHLAEUFER sind: die
Marke lag beim Fill TIEFER (LONG) bzw. HOEHER (SHORT) als bei der Armierung, und
der Kurs hat die urspruengliche Marke nie erreicht. Genau der Fall des Users.
⚠ KEIN NACHBAU: die Kompression kommt aus der echten `wave_rec._squeeze_one`,
der Exit aus `exit_model.simulate` (kanonisch), Kosten aus dem Bar-Spread.
REGEL VORAB FIXIERT: "Einfrieren" ist nur dann eine Verbesserung, wenn B in
BEIDEN Halbjahren einen hoeheren OeR liefert UND die Nachlaeufer-Quote in A
substanziell ist (>10 %). Ist die Quote klein, ist der Live-Fall ein Einzelfall
und nichts zu aendern.
Aufruf: python backtest_squeeze_frozen.py [bars]
"""
import sys
import MetaTrader5 as mt5
from backtest_cost_gate import kennzahlen
from core.exit_model import LIVE, simulate
from core.squeeze_scan import atr_series
from core.wave_rec import WaveRecommender
ATRMIN = 0.06 # LIVE-Wert (nicht der Modul-Default 0,12)
MAX_WARTE = 24 # Bars, die eine armierte Marke hoechstens gilt
def zustand(H, L, C, A, i):
"""Echte `_squeeze_one`-Auswertung fuer Bar i (Fenster wie live)."""
lo = max(0, i - 40)
return WaveRecommender._squeeze_one(H[lo:i + 1], L[lo:i + 1],
C[lo:i + 1], A[i])
def lauf(H, L, C, A, SP, von, bis, ende, modus):
"""modus 'nach' = Marke wandert (live) · 'fix' = beim Armieren eingefroren."""
rs, nachlaeufer, gesamt = [], 0, 0
i = von
marke = None # (preis, richtung, bar_armiert, preis_bei_armierung)
while i < bis:
atr = A[i]
if not atr or atr < ATRMIN:
i += 1; marke = None; continue
z = zustand(H, L, C, A, i)
if z.get("state") is None:
marke = None; i += 1; continue
lvl, d = z.get("level"), (1 if z.get("dir") == "LONG" else -1)
if lvl is None:
i += 1; continue
if marke is None or marke[1] != d or i - marke[2] > MAX_WARTE:
marke = (lvl, d, i, lvl) # neu armieren
elif modus == "nach":
marke = (lvl, d, marke[2], marke[3]) # Marke nachziehen
# Fill? Beruehrung der AKTUELL gueltigen Marke im naechsten Bar
preis = marke[0]
j = i + 1
if j >= ende:
break
if ((H[j] >= preis) if d > 0 else (L[j] <= preis)) and j + 1 < ende:
gesamt += 1
# Nachlaeufer: Marke ist dem Kurs entgegengekommen UND die
# urspruengliche Marke wurde nie erreicht.
urspr = marke[3]
entgegen = (preis < urspr - 1e-9) if d > 0 else (preis > urspr + 1e-9)
nie_erreicht = (max(H[marke[2]:j + 1]) < urspr) if d > 0 \
else (min(L[marke[2]:j + 1]) > urspr)
if entgegen and nie_erreicht:
nachlaeufer += 1
r = simulate(preis, d, atr, H, L, C, j + 1, LIVE,
timestop=True, use_tp=True)
rs.append(r - ((SP[j] if SP[j] > 0 else 0.0225) / atr))
marke = None
i = j + 3 # kleiner Cooldown wie live
continue
i += 1
return rs, nachlaeufer, gesamt
def zeig(lbl, k, nl, ges):
if not k:
print(" {:<24} --".format(lbl)); return
ci = k.get("ci"); m = ""
if ci:
m = (" [{:+.3f} .. {:+.3f}]".format(ci[0], ci[1])
+ ("" if ci[0] * ci[1] > 0 else " ~0"))
quote = (100.0 * nl / ges) if ges else 0.0
print(" {:<24} n={:>4} WR={:>3.0f}% OeR={:+.3f} PF={:.2f}{} Nachlaeufer {:.0f} %"
.format(lbl, k["n"], k["wr"], k["oer"], k["pf"], m, quote))
def main():
n = int(sys.argv[1]) if len(sys.argv) > 1 else 40000
mt5.initialize()
sym = next((c for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD")
if mt5.symbol_info(c)), None)
bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n)
point = mt5.symbol_info(sym).point
mt5.shutdown()
H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars]
C = [float(b["close"]) for b in bars]
SP = [float(b["spread"]) * point for b in bars]
A = atr_series(H, L, C)
N = len(C); mid = N // 2; ende = N - LIVE.max_hold - 1
print("=" * 100)
print(" WANDERNDE gegen EINGEFRORENE Squeeze-Marke — {} M5 ({} Bars)".format(sym, N))
print(" echte `_squeeze_one` · kanonischer Exit · Echtkosten · 95-%-KI")
print("=" * 100)
for modus, titel in (("nach", "A) NACHGEZOGEN (= live)"),
("fix", "B) EINGEFROREN beim Armieren")):
print("\n " + titel)
print(" " + "-" * 96)
for lbl, (lo, hi) in (("H1", (0, mid)), ("H2", (mid, ende))):
rs, nl, ges = lauf(H, L, C, A, SP, lo, hi, ende, modus)
zeig(lbl, kennzahlen(rs), nl, ges)
print("\n " + "=" * 96)
print(" LESART: 'Nachlaeufer' = die Marke kam dem Kurs ENTGEGEN und die")
print(" urspruengliche Marke wurde nie erreicht — also kein Ausbruch.")
print(" Ist die Quote klein, war der Live-Fall ein Einzelfall.")
if __name__ == "__main__":
main()