Pending-Fill: Bot-Trades wurden nicht als solche gebucht

Auftrag "überprüfe die heutigen autotrades": die DB meldete für den 06.08.
null Bot-Trades, das Log vier gefüllte SQZ-STOP-Orders. Beides stimmte.

Ein Pending-Fill läuft nicht über engine._open — die Position wird von
trader._refresh_locked per magic-match adoptiert, und dieser Pfad loggt nur
Ticket/Symbol/Richtung/Lots/Preis. Es fehlten setup, sl_at_entry und alle
16 ctx_*-Spalten. Damit war genau die Messpipeline, die den Pending-Umbau
vom 05.08. kontrollieren soll, blind für ihn (B4-Monitor, squeeze_b5,
squeeze_entry_gap zählten null) — Deployment-Drift Fall 8, erste Variante,
bei der nicht die Strategie driftet, sondern ihre Beobachtbarkeit.

- history.tag_bot_trade(): UPDATE nur wenn setup IS NULL, stiller Fail-open
- engine._pending_tickets: Ticket -> Quelle, beim Platzieren/Stornieren gepflegt
- engine._check_pending_fill() im _pos_loop vor dem Pending-Manager;
  Zuordnung über Positions-Nr == Order-Nr (an allen 4 Fills verifiziert),
  setzt zusätzlich _bot_open_ticket/_source und die Entry-Zähler

6 Szenarien getestet, darunter die zwei gefährlichen: fremde Position wird
nicht getaggt, derselbe Fill zählt bei Folge-Ticks nur einmal.
Die 4 Trades des 06.08. nachgetragen (Backup .bak-2026-08-06).

ctx_*/sl_at_entry bewusst NICHT nachgetragen: rekonstruiert wären sie später
von gemessenen Werten nicht unterscheidbar.

Dabei gefunden, dokumentiert, NICHT behoben: der Signal-Pfad setzt/storniert
dieselbe Order 4x in 76 s bei identischem Level, weil block_reason zwischen
entry_room und breakout_pending pendelt (Drift Fall 9).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-06 10:55:41 +02:00
co-authored by Claude Opus 5
parent 730219fce8
commit 3c1442109d
3 changed files with 147 additions and 0 deletions
+61
View File
@@ -355,6 +355,11 @@ class TradingEngine:
"signal_pending_entry", "true").lower() == "true"
self._pending_levels = None
self._pending_quelle = ""
# Welche Pending-Order stammt von welchem Pfad? Bei einem FILL ist die
# Positions-Nummer identisch mit der Order-Nummer (MT5) — nur so laesst
# sich die adoptierte Position nachtraeglich dem Bot zuordnen.
self._pending_tickets: dict[int, str] = {}
self._pending_tagged: set = set()
self._squeeze_pending_err = None
self._squeeze_chase_logged = None
self._signal_chase_logged = None
@@ -707,6 +712,7 @@ class TradingEngine:
# ZUERST die ruhenden Stop-Orders pflegen (setzen/stornieren),
# DANN der Market-Fallback: so kann eine bereits liegende Order
# den Ausbruch am Level abfangen, bevor nachgejagt würde.
self._check_pending_fill() # gefuellte Pending-Order zuordnen
self._manage_squeeze_pending()
self._check_auto_squeeze() # autonomer Entry auf Squeeze-Ausbruch
self._check_auto_signal() # autonomer Entry auf die Empfehlung
@@ -1055,6 +1061,58 @@ class TradingEngine:
"""Alias — `_check_auto_squeeze` ruft weiterhin diesen Namen."""
return self._auto_guard("squeeze")
def _check_pending_fill(self):
"""Hat eine von UNS platzierte Pending-Order gefuellt? Dann nachtragen.
⚠ WARUM: eine gefuellte Pending-Order laeuft NICHT durch `_open` — MT5
oeffnet die Position selbst, und `trader._refresh_locked` adoptiert sie per
magic-match und loggt sie OHNE Setup. Am 06.08. fuellten drei `SQZ-STOP`-
Orders und landeten als `setup=NULL` in der DB, also ununterscheidbar von
manuellen Trades. Damit zaehlten der B4-Monitor und die Reminder
`squeeze_b5` / `squeeze_entry_gap` **null Bot-Trades**, obwohl der Bot
gehandelt hatte — die Messpipeline war blind fuer genau den Umbau, den sie
kontrollieren soll.
Die Zuordnung ist eindeutig: bei einem Pending-Fill ist die POSITIONS-Nummer
identisch mit der ORDER-Nummer (in den Broker-Daten vom 06.08. verifiziert).
"""
try:
if not self._pending_tickets:
return
ps = self.trader.snapshot()
tk = ps.get("ticket")
if not tk or tk in self._pending_tagged:
return
quelle = self._pending_tickets.get(int(tk))
if not quelle:
return # fremde/manuelle Position
d = "LONG" if ps.get("order_type") == 0 else "SHORT"
setup = f"SQUEEZE_{d}" if quelle == "squeeze" else f"AUTOSIG_{d}"
self._pending_tagged.add(tk)
self._pending_tickets.pop(int(tk), None)
ok = self.history.tag_bot_trade(int(tk), setup) if self.history else False
# Dieselbe Buchfuehrung wie beim Market-Pfad, damit B4/B5 zaehlen
self._bot_open_ticket = tk
self._bot_open_source = "auto_squeeze" if quelle == "squeeze" else "auto_signal"
if quelle == "squeeze":
self._squeeze_entry_count += 1
else:
self._signal_entry_count += 1
log.info(f"🎯 PENDING-FILL ({quelle}): {d} @ {ps.get('entry_price')} "
f"— Position {tk} als {setup} getaggt"
+ ("" if ok else " ⚠ DB-Nachtrag fehlgeschlagen"))
tg = self.cfg["telegram"]
if tg.get("enabled", "false").lower() == "true" and tg.get("bot_token"):
try:
send_telegram(f"🎯 <b>Pending-Fill ({quelle})</b>\n"
f"{self.data.symbol or ''} {d} @ "
f"{ps.get('entry_price')} — ruhende Stop-Order "
f"am Level ausgeloest.",
tg["bot_token"], tg["chat_id"])
except Exception as e:
log.warning(f"Pending-Fill-Telegram: {e}")
except Exception as e:
log.error(f"_check_pending_fill: {e}", exc_info=True)
def _pending_ziel(self) -> tuple[dict, str]:
"""Welche Stop-Orders SOLLEN liegen? → ({order_type: preis}, Quelle).
@@ -1125,6 +1183,7 @@ class TradingEngine:
for o in offen:
err = self.trader.cancel_pending(o.ticket)
if not err:
self._pending_tickets.pop(int(o.ticket), None)
log.info(f"⏸ Pending-Entry storniert (T={o.ticket}) — "
+ ("Position offen" if ps.get("ticket")
else "Setup nicht mehr gültig"))
@@ -1145,6 +1204,7 @@ class TradingEngine:
vorhanden[o.type] = o.ticket
else:
self.trader.cancel_pending(o.ticket)
self._pending_tickets.pop(int(o.ticket), None)
for otype, preis in ziel.items():
if otype in vorhanden:
continue
@@ -1154,6 +1214,7 @@ class TradingEngine:
# gleiche Regel wie beim Market-Pfad (s. `_open`)
atr_tf=(mt5.TIMEFRAME_M5 if quelle == "squeeze" else None))
if t:
self._pending_tickets[int(t)] = quelle
log.info(f"🎯 Pending-Entry ({quelle}) gesetzt: "
f"{'BUY_STOP' if otype == mt5.ORDER_TYPE_BUY_STOP else 'SELL_STOP'}"
f" @ {preis:.3f} (T={t})")
+26
View File
@@ -576,6 +576,32 @@ class HistoryLogger:
return (int(since.timestamp()), now)
return (0, now)
def tag_bot_trade(self, ticket: int, setup: str) -> bool:
"""Setup einer bereits geloggten Position NACHTRAGEN. → True bei Erfolg.
⚠ WARUM DAS NOETIG IST: eine gefuellte PENDING-Order laeuft nicht durch
`engine._open`, sondern wird vom Positions-Abgleich als „magic-match"
adoptiert und mit `log_trade_open(...)` OHNE Setup geschrieben. Am 06.08.
fuellten drei `SQZ-STOP`-Orders und landeten dadurch als `setup=NULL` in der
DB — ununterscheidbar von manuellen Trades. Folge: B4-Monitor und die
Reminder `squeeze_b5` / `squeeze_entry_gap` zaehlten 0, obwohl der Bot
gehandelt hatte. Genau die Messpipeline, die den Pending-Umbau
kontrollieren soll, war blind fuer ihn.
Nur setzen, wenn noch KEIN Setup steht — ein manuell/regulaer getaggter
Trade darf nicht ueberschrieben werden."""
try:
with self._connect() as conn:
cur = conn.execute(
"UPDATE trades SET setup=? WHERE ticket=? AND setup IS NULL",
(setup, int(ticket)))
return cur.rowcount > 0
except Exception:
# ⚠ `core/history.py` fuehrt bewusst keinen Logger (Konvention der
# Datei: stilles Abfangen). Ein fehlgeschlagenes Nachtragen ist
# unkritisch — der Trade steht in der DB, nur ohne Setup-Tag; der
# Aufrufer sieht das am Rueckgabewert.
return False
def stats_overview(self, period: str = "all") -> dict:
"""Liefert ein Dict mit den Hauptkennzahlen für den gewählten Zeitraum."""
since, until = self._range_to_ts(period)