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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4ebc55c49b
commit
d87bda1461
@@ -0,0 +1,158 @@
|
|||||||
|
"""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()
|
||||||
@@ -865,6 +865,7 @@ class TradingEngine:
|
|||||||
# erkannt und die Benachrichtigung vorgemerkt.
|
# erkannt und die Benachrichtigung vorgemerkt.
|
||||||
self._check_close_notify() # Telegram bei jedem Close ab Schwelle
|
self._check_close_notify() # Telegram bei jedem Close ab Schwelle
|
||||||
self._check_open_notify() # Telegram bei jeder Eröffnung
|
self._check_open_notify() # Telegram bei jeder Eröffnung
|
||||||
|
self._retry_tag_nachtrag() # gescheiterte setup-Nachtraege
|
||||||
self._check_slot_reichweite() # meldet ungedeckte 2. Position
|
self._check_slot_reichweite() # meldet ungedeckte 2. Position
|
||||||
self._check_adverse15() # 15-Min-Regel (nur Bot-Trades)
|
self._check_adverse15() # 15-Min-Regel (nur Bot-Trades)
|
||||||
self._check_flip_close() # Empfehlung dreht → schließen
|
self._check_flip_close() # Empfehlung dreht → schließen
|
||||||
@@ -949,6 +950,41 @@ class TradingEngine:
|
|||||||
return False
|
return False
|
||||||
return self._circuit_tripped_date == datetime.now(_BERLIN).date()
|
return self._circuit_tripped_date == datetime.now(_BERLIN).date()
|
||||||
|
|
||||||
|
def _retry_tag_nachtrag(self):
|
||||||
|
"""Gescheiterte `setup`-Nachträge erneut versuchen (2026-08-20).
|
||||||
|
|
||||||
|
⚠ Der Fill wird aus dem Positions-Snapshot erkannt, die DB-Zeile
|
||||||
|
schreibt aber ein anderer Pfad. Ist der beim ersten Versuch noch nicht
|
||||||
|
durch, trifft das UPDATE keine Zeile. Ohne Wiederholung bliebe der Trade
|
||||||
|
dauerhaft `setup=NULL` — und damit unsichtbar für B4-Monitor,
|
||||||
|
`squeeze_b5` und `squeeze_entry_gap`.
|
||||||
|
⚠ Nach 60 Versuchen (~1 min) aufgeben und LAUT melden: eine Zeile, die
|
||||||
|
nach einer Minute nicht existiert, kommt nicht mehr — dann ist etwas
|
||||||
|
anderes kaputt, und das soll nicht still bleiben.
|
||||||
|
"""
|
||||||
|
offen = getattr(self, "_tag_nachtrag", None)
|
||||||
|
if not offen or not self.history:
|
||||||
|
return
|
||||||
|
if not hasattr(self, "_tag_versuche"):
|
||||||
|
self._tag_versuche = {}
|
||||||
|
for tk in list(offen):
|
||||||
|
setup = offen[tk]
|
||||||
|
try:
|
||||||
|
if self.history.tag_bot_trade(int(tk), setup):
|
||||||
|
offen.pop(tk, None)
|
||||||
|
self._tag_versuche.pop(tk, None)
|
||||||
|
log.info(f"✅ DB-Nachtrag erfolgreich: T={tk} → {setup}")
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(f"_retry_tag_nachtrag: {e}")
|
||||||
|
n = self._tag_versuche.get(tk, 0) + 1
|
||||||
|
self._tag_versuche[tk] = n
|
||||||
|
if n >= 60:
|
||||||
|
offen.pop(tk, None)
|
||||||
|
self._tag_versuche.pop(tk, None)
|
||||||
|
log.warning(f"⚠ DB-Nachtrag T={tk} nach {n} Versuchen aufgegeben "
|
||||||
|
f"— der Trade bleibt setup=NULL und fehlt in B4/B5.")
|
||||||
|
|
||||||
def _check_open_notify(self):
|
def _check_open_notify(self):
|
||||||
"""Telegram bei JEDER Eröffnung — beide Slots (User-Vorgabe 2026-08-19).
|
"""Telegram bei JEDER Eröffnung — beide Slots (User-Vorgabe 2026-08-19).
|
||||||
|
|
||||||
@@ -1481,6 +1517,22 @@ class TradingEngine:
|
|||||||
self._pending_tagged.add(tk)
|
self._pending_tagged.add(tk)
|
||||||
self._pending_tickets.pop(int(tk), None)
|
self._pending_tickets.pop(int(tk), None)
|
||||||
ok = self.history.tag_bot_trade(int(tk), setup) if self.history else False
|
ok = self.history.tag_bot_trade(int(tk), setup) if self.history else False
|
||||||
|
# ⚠⚠ RENNEN, real am 2026-08-20: der Fill wird hier aus
|
||||||
|
# `trader_brk.snapshot()` erkannt, die DB-ZEILE schreibt aber ein
|
||||||
|
# anderer Pfad (`trader._refresh_locked` → `log_trade_open`). Ist
|
||||||
|
# der noch nicht durch, trifft das UPDATE keine Zeile und der
|
||||||
|
# Nachtrag scheitert STILL — der Trade bleibt `setup=NULL` und ist
|
||||||
|
# von einem manuellen nicht unterscheidbar. Genau die
|
||||||
|
# Buchfuehrungs-Luecke, gegen die `tag_bot_trade` gebaut wurde
|
||||||
|
# (Deployment-Drift Fall 8), nur eine Ebene tiefer.
|
||||||
|
# ⚠ Der Fall ist NICHT theoretisch: T=50371599 lag danach mit
|
||||||
|
# `setup=NULL` in der DB, obwohl die Zeile Sekunden spaeter da war.
|
||||||
|
# Deshalb nachreichen statt aufgeben — dasselbe Retry-Muster wie
|
||||||
|
# beim Gewinn-Close (`_sr_close_min_gain_armed_ticket`, 23.07.).
|
||||||
|
if not ok:
|
||||||
|
if not hasattr(self, "_tag_nachtrag"):
|
||||||
|
self._tag_nachtrag = {}
|
||||||
|
self._tag_nachtrag[int(tk)] = setup
|
||||||
# Dieselbe Buchfuehrung wie beim Market-Pfad, damit B4/B5 zaehlen
|
# Dieselbe Buchfuehrung wie beim Market-Pfad, damit B4/B5 zaehlen
|
||||||
self._bot_open_ticket = tk
|
self._bot_open_ticket = tk
|
||||||
self._bot_open_source = "auto_squeeze"
|
self._bot_open_source = "auto_squeeze"
|
||||||
|
|||||||
Reference in New Issue
Block a user