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:
co-authored by
Claude Opus 5
parent
730219fce8
commit
3c1442109d
@@ -1882,6 +1882,64 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
|
||||
entry_gap.py` rechnet DST-korrekt über `zoneinfo` und liefert **+0,275 / +0,686**
|
||||
(n=34 unverändert). Gültig sind die zweiten Werte; die Schlussfolgerung ändert
|
||||
sich nicht.
|
||||
- **⚠⚠ EIN PENDING-FILL LIEF AN DER GESAMTEN BUCHFÜHRUNG VORBEI (Fix 2026-08-06,
|
||||
User-Auftrag „überprüfe die heutigen autotrades").** Der Befund kam nur zustande,
|
||||
weil die Frage überhaupt gestellt wurde: die DB meldete für den 06.08. **null
|
||||
Bot-Trades**, das Log dagegen vier gefüllte `SQZ-STOP`-Orders. Beides stimmte.
|
||||
**Mechanismus:** eine ruhende Stop-Order füllt **im Broker**, nicht über
|
||||
`engine._open`. Die Position wird erst später von `trader._refresh_locked` per
|
||||
magic-match **adoptiert** — und dieser Pfad ruft `history.log_trade_open()` nur mit
|
||||
Ticket/Symbol/Richtung/Lots/Preis auf. Es fehlten damit **alle** Felder, die der
|
||||
Market-Pfad setzt: `setup`, `sl_at_entry` und die **16 `ctx_*`-Spalten** der
|
||||
Telemetrie-Stufe 1. Vier reale Trades waren so von manuellen **nicht
|
||||
unterscheidbar**.
|
||||
⚠ **Die Tragweite ist größer als „ein NULL-Feld":** genau die Messpipeline, die den
|
||||
Pending-Umbau vom 05.08. kontrollieren soll, war **blind für ihn** — B4-Monitor,
|
||||
`squeeze_b5` (Abbruchregel!) und `squeeze_entry_gap` zählten null Trades, und
|
||||
`sl_at_entry` fehlte ausgerechnet bei den Trades, an denen die M5-SL-Umstellung zu
|
||||
prüfen war. **Deployment-Drift Fall 8**, und die erste Variante, bei der nicht die
|
||||
Strategie driftet, sondern **ihre Beobachtbarkeit**. Ein Umbau, der den Bestellweg
|
||||
ändert, ändert auch, welcher Code die Bücher führt — das gehört beim Umbau geprüft.
|
||||
**Fix:** `history.tag_bot_trade(ticket, setup)` (UPDATE nur wenn `setup IS NULL`,
|
||||
stiller Fail-open — `core/history.py` hat bewusst keinen Logger) +
|
||||
`engine._pending_tickets` (Ticket → Quelle, beim Platzieren gesetzt, beim
|
||||
Stornieren/Taggen entfernt) + `_check_pending_fill()` im `_pos_loop` VOR dem
|
||||
Pending-Manager. Die Zuordnung ist eindeutig, weil bei einem Pending-Fill die
|
||||
**Positions-Nummer identisch mit der Order-Nummer** ist (an allen vier
|
||||
Broker-Datensätzen des 06.08. verifiziert) — kein Zeitfenster-Raten. Der Fix setzt
|
||||
zusätzlich `_bot_open_ticket`/`_bot_open_source` und die Entry-Zähler, damit
|
||||
15-Min-Regel, B4-Monitor und Snapshot wieder dieselbe Wahrheit sehen.
|
||||
Mit **6 Szenarien getestet**, darunter die beiden gefährlichen: fremde/manuelle
|
||||
Position wird NICHT getaggt, und derselbe Fill zählt bei wiederholten Ticks nur
|
||||
**einmal**. Die vier Trades des 06.08. wurden nachgetragen (DB-Backup
|
||||
`oil_widget_history.db.bak-2026-08-06`).
|
||||
⚠ **Bewusst NICHT nachgetragen: `ctx_*` und `sl_at_entry`.** Die stehen für den
|
||||
Zustand **im Moment des Fills** und lassen sich nachträglich nur rekonstruieren,
|
||||
nicht messen — eine rekonstruierte Zahl in einer Telemetrie-Spalte wäre später von
|
||||
einer gemessenen nicht mehr unterscheidbar. Ab jetzt sind sie für Pending-Fills
|
||||
weiterhin leer; wer sie braucht, muss den Kontext beim **Platzieren** der Order
|
||||
einfrieren (offen, nicht gebaut).
|
||||
- **⚠ OFFEN: Pending-Churn im SIGNAL-Pfad (gefunden 06.08., nicht behoben).** Am
|
||||
06.08. wurde dieselbe SELL_STOP viermal in 76 s gesetzt und storniert — **bei
|
||||
identischem Level 74,981**, es wandert also nichts. Die Ursache steht in
|
||||
`recommendations.block_reason`: der Grund **pendelt zwischen `entry_room` und
|
||||
`breakout_pending`**. Sobald das Raum-Gate den Kurs kurz stummschaltet, liefert
|
||||
`_pending_ziel()` `{}` und der Manager storniert; eine Sekunde später ist das Gate
|
||||
wieder offen und die Order wird neu gelegt.
|
||||
⚠ **Das ist wieder Deployment-Drift, nicht nur Log-Lärm:** `backtest_auto_signal_v3.py`
|
||||
hat den Pending-Vorteil (ØR +0,35/+0,32 gegen −0,09/−0,11 am Markt) unter der
|
||||
Annahme gemessen, dass die Order **durchgehend am Level liegt**. Flackert sie im
|
||||
5-s-Takt, geht genau der Ausbruch verloren, der in eine Storno-Lücke fällt — und
|
||||
das ist der Fall, für den sie da ist.
|
||||
**Lösungsrichtung (ungebaut):** harte von weichen Storno-Gründen trennen. HART
|
||||
(sofort stornieren) = Position offen · Startup-Schonfrist · Guard (Nacht/Circuit/
|
||||
News) · Richtungswechsel · Level wandert über die Toleranz · Feature aus. WEICH
|
||||
(Order liegen lassen) = kurzzeitiges Stummschalten durch `entry_room`/`min_conf`/
|
||||
`breakout_pending`. Argument dafür: die Order füllt nur, wenn der Kurs das Level
|
||||
**bricht** — dann hat er sich vom Gegenlevel wegbewegt, die Raum-Bedingung ist im
|
||||
Fill-Moment also besser als im Flacker-Moment. Argument dagegen: das ist eine
|
||||
Aussage über den Einstiegs-Filter und damit **nicht rein infrastrukturell** —
|
||||
vor dem Bau messen (Variante „Order bleibt liegen" gegen „Order flackert").
|
||||
- **Auto-Squeeze-Entry = LIVE AUTONOM (User-Vorgabe 2026-07-16, `[trading]
|
||||
auto_squeeze=true`, Default FALSE):** der Bot eröffnet **selbständig eine echte
|
||||
Order**, sobald der **gemessen-validierte** Squeeze-Ausbruch feuert (`wave.squeeze.
|
||||
@@ -3374,6 +3432,8 @@ Bedingungen BETRIEBEN als VALIDIERT. An EINEM Tag wurden drei Fälle gefunden:
|
||||
| 7 | Auto-Squeeze-**Einstiegspreis** | Fill **AM Ausbruchs-Level** (`backtest_breakout_squeeze.py`) | **MARKET**-Order, live median **+0,275×ATR** über dem Level (34 echte Entries) | ØR **+0,456 → −0,158** (H1) bzw. **+0,611 → −0,006** (H2); WR 56 → 37 % |
|
||||
| 6 | `auto_squeeze_skip_night` | Nacht-Kostenfalle am **Wellensignal** (alle Nacht-Bars: 0,333×ATR) | **Squeeze**-Ausbrüche, die nur bei anziehendem ATR feuern (0,183×ATR = Tag-Niveau) | Gate kostet ΣR **−42 (H1) / −243 (H2)**; geblockte Ausbrüche sind in beiden Hälften positiv |
|
||||
| 5 | `backtest_auto_signal.py` | **ohne Winkel** (`_build` ohne `angle=` → Default 90 → `ad=0`) | Winkel steuert einen **Signalzweig** (Reversal) UND **±15 Konfidenzpunkte** | Urteil vom 30.07. beschrieb weder das alte noch das neue System; der Winkelterm allein hebt H1 bei conf 75 von **−0,123 auf −0,047** |
|
||||
| **8** | **Buchführung der Bot-Trades** (nicht die Strategie, ihre **Beobachtbarkeit**) | Einstieg läuft über `engine._open` → schreibt `setup` + `sl_at_entry` + 16 `ctx_*` | **Pending-Fill** füllt im Broker, `trader._refresh_locked` adoptiert und loggt nur 5 Felder | 4 reale Squeeze-Trades am 06.08. als `setup=NULL` = **von manuellen ununterscheidbar**; B4-Monitor, `squeeze_b5` (Abbruchregel!) und `squeeze_entry_gap` zählten **null** |
|
||||
| **9** | **Pending-Order liegt am Level** (offen) | `backtest_auto_signal_v3.py`: Order liegt **durchgehend** (ØR +0,35/+0,32) | `entry_room`↔`breakout_pending` flackern → 4× setzen/stornieren in 76 s bei identischem Level | Ausbruch in einer Storno-Lücke = Entry ganz verloren; Ausmaß **ungemessen** |
|
||||
|
||||
Keiner wäre durch MEHR Backtesting gefunden worden — die Backtests waren korrekt.
|
||||
**Ursachen (strukturell):** (a) Backtests sind NACHBAUTEN, keine Nutzer des Live-Codes
|
||||
|
||||
@@ -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})")
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user