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>
159 lines
6.8 KiB
Python
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()
|