From fd2ca46dc0d104f06372beef2df0b8b7f3a93c74 Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Thu, 20 Aug 2026 20:29:26 +0200 Subject: [PATCH] Ruhende Orders: aufraeumen statt liegenlassen (User-Vorgabe) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User: "was sind ruhende orders. Loesche die wenn der trade nicht eroeffnet werden kann." Anlass: MT5 zeichnete SL/TP-Linien, obwohl keine Position lief. Das waren die liegenden Stop-Orders des Breakout-Traders - MT5 stellt jede Pending-Order samt mitgeschicktem SL selbst dar. Der Bot exportiert NIE SL/TP ins Chart (0 Treffer im .mq5, keine SL-Zeile in der CSV). Kein Anzeigefehler. ZWEI ECHTE VERWAISUNGS-PFADE GEFUNDEN. _manage_squeeze_pending stieg VOR dem Aufraeum-Block aus, wenn squeeze_pending_entry=false oder kein Symbol da war. Die Orders sind ORDER_TIME_GTC - sie verfallen NICHT. Eine so verwaiste Order haengt beim Broker, wird von niemandem mehr bewertet und kann Stunden spaeter fuellen: ohne Dedup, ohne Guard-Pruefung und (bis heute) ohne Trailing. Neu engine._pendings_abraeumen(sym, grund) - beide Pfade raeumen jetzt ab. DIE EIGENTLICHE VORGABE: ein halbes Setup wird aufgeloest. Scheitert eine Seite mit Lot-Fehler (freie Margin reicht nicht fuers Mindestlot), wird auch die Gegenseite storniert - der gemessene Squeeze ist ein Ausbruch in BEIDE Richtungen; laege nur eine Seite, handelte der Bot ihn nur noch in eine. Ausdruecklich NICHT bei "Kurs hat das Level bereits passiert": das ist voruebergehend und betrifft nur die eine Seite. Der Lot-Fehler betrifft das KONTO und damit beide. Erkannt ueber die Konstante mt5_utils.LOT_ZU_KLEIN, nicht ueber ein deutsches Wort im Fliesstext - eine Erkennung, die an einer Formulierung haengt, hoert still auf zu funktionieren und faellt dabei auf die stille Seite. ⚠⚠ DIE LOG-DROSSEL WAR AM SELBEN TAG ZUM ZWEITEN MAL AUSGEHEBELT - durch meine eigene Verbesserung. Vormittags lag der Merker in EINER Variable fuer beide Seiten (behoben). Dann bekam die Meldung ueber lot_grund ihre Zahlen, und die freie Margin aendert sich mit jedem Tick: der Text war jedes Mal neu, der Vergleich traf immer zu, wieder 2 Zeilen pro Sekunde. Gedrosselt wird jetzt auf die URSACHE, nicht auf ihre Formulierung (_err_key: alle Zahlen -> #). Gemessen: 7 Zeilen in 90 s statt ~180, und die verbliebenen sind echte Wechsel zwischen den beiden Ursachen. Lehre: eine Drossel, die einen TEXT vergleicht, ist so stabil wie der Text. Live verifiziert: 0 liegende Orders, keine verwaisten Reste, Startup-Schonfrist raeumt korrekt ab, Orders kommen danach zurueck sobald die Margin reicht. Pipeline gruen (A-G), 97 Tests, deployt mit --feld squeeze_block. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 51 +++++++++++++++++++++++++++++ core/engine.py | 83 ++++++++++++++++++++++++++++++++++++++++++++--- core/mt5_utils.py | 19 ++++++++--- 3 files changed, 143 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ba8a743..21189e0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9342,6 +9342,57 @@ mit Begründung im Code, damit es nicht als Versehen gelesen wird. **Pipeline jetzt: A · A2 · A3 · B · B2 · C · C2 · D · E · F · G** — alle grün, 97 Tests, deployt und verifiziert. +## ⚠⚠ RUHENDE ORDERS: AUFRÄUMEN STATT LIEGENLASSEN (User-Vorgabe 2026-08-20) + +User: *„was sind ruhende Orders. Lösche die, wenn der Trade nicht eröffnet +werden kann."* — Anlass war, dass MT5 SL/TP-Linien zeichnete, obwohl keine +Position lief. **Das waren die liegenden Stop-Orders des Breakout-Traders**; +MT5 stellt jede Pending-Order samt ihrem mitgeschickten SL selbst dar. ✅ Der +Bot exportiert **nie** SL/TP ins Chart (0 Treffer im `.mq5`, keine `SL;`-Zeile +in der CSV) — kein Anzeigefehler, sondern eine Broker-Funktion. + +⚠⚠ **DIE PRÜFUNG DER FRAGE FÖRDERTE ZWEI ECHTE VERWAISUNGS-PFADE ZUTAGE.** +`_manage_squeeze_pending` stieg **VOR** dem Aufräum-Block aus, wenn +`squeeze_pending_entry=false` **oder** kein Symbol da war (MT5-Aussetzer). Die +Orders sind `ORDER_TIME_GTC` — **sie verfallen nicht**. Eine so verwaiste Order +hängt beim Broker, wird von niemandem mehr bewertet und kann Stunden später +füllen: **ohne Dedup, ohne Guard-Prüfung** und (bis zum Fix vom selben Tag) +**ohne Trailing**. +✅ Neu `engine._pendings_abraeumen(sym, grund)` — beide Pfade räumen jetzt ab, +statt nur zurückzukehren. + +✅ **UND DIE EIGENTLICHE VORGABE UMGESETZT: ein halbes Setup wird aufgelöst.** +Scheitert eine Seite mit **Lot-Fehler** (die freie Margin reicht nicht einmal +fürs Mindestlot), wird auch die **Gegenseite** storniert. Grund: der gemessene +Squeeze ist ein Ausbruch aus der Box in **beide** Richtungen; läge nur eine +Seite, handelte der Bot ihn nur noch in **eine** — das ist nicht das Setup, das +validiert wurde. +⚠ **Ausdrücklich NICHT bei „Kurs hat das Level bereits passiert"**: das ist +vorübergehend und betrifft naturgemäß nur die eine Seite (der Kurs steht gerade +an der Box-Kante); die Gegenseite bleibt ein gültiger Einstieg. Der Lot-Fehler +dagegen betrifft das **Konto** und damit beide Seiten. +⚠ Erkannt über die **Konstante `mt5_utils.LOT_ZU_KLEIN`**, nicht über ein +deutsches Wort im Fließtext: eine Erkennung, die an einer Formulierung hängt, +hört still auf zu funktionieren, sobald jemand den Satz umschreibt — und fällt +dabei auf die **stille** Seite (kein Aufräumen), nicht auf die laute. + +⚠⚠⚠ **DIE LOG-DROSSEL WAR AM SELBEN TAG ZUM ZWEITEN MAL AUSGEHEBELT — durch +meine eigene Verbesserung.** Vormittags lag der Merker in EINER Variable für +beide Order-Seiten (behoben). Dann bekam die Meldung über `lot_grund` ihre +Zahlen — und die **freie Margin ändert sich mit jedem Tick**. Der Text war also +jedes Mal neu, der Vergleich `!= err` traf immer zu: wieder **2 Zeilen pro +Sekunde**. +✅ Gedrosselt wird jetzt auf die **URSACHE**, nicht auf ihre Formulierung +(`_err_key`: alle Zahlen → `#`). Gemessen: **7 Zeilen in 90 s** statt ~180 — +und die verbliebenen sind echte Wechsel zwischen den beiden Ursachen, kein Lärm. +⚠ **Lehre: eine Drossel, die einen TEXT vergleicht, ist so stabil wie der Text.** +Sobald eine Meldung Messwerte enthält, muss auf einen normalisierten Schlüssel +gedrosselt werden. + +✅ Live verifiziert nach dem Deploy: **0 liegende Orders**, keine verwaisten +Reste, Startup-Schonfrist räumt korrekt ab und die Orders kommen danach zurück, +sobald die Margin reicht. + ## ⚠⚠⚠ P(break) IST LIVE **INVERTIERT** — und es steuert echtes Geld (2026-08-19) Die fällige Messung ist entscheidbar geworden: **n=530 entkoppelt** gegen die diff --git a/core/engine.py b/core/engine.py index b2f77aa..6c810fc 100644 --- a/core/engine.py +++ b/core/engine.py @@ -13,6 +13,7 @@ das MT5-Terminal. Es sollte immer nur EINE Engine-Instanz gegen das Terminal laufen. """ from __future__ import annotations +import re import threading import time from datetime import datetime @@ -30,6 +31,7 @@ from core.config import ( # und die `[zones]`-Sektion in der ini erhalten — reaktivierbar, falls je gewollt. from core.logger import get_logger from core.history import HistoryLogger +from core.mt5_utils import LOT_ZU_KLEIN from core.mt5data import MT5Data from core.trader import TradeManager from core.trailing import TrailingManager @@ -202,6 +204,18 @@ def send_telegram(text, *a, **kw): return _send_telegram_roh(text, *a, **kw) +def _err_key(msg: str) -> str: + """Fehlermeldung auf ihre URSACHE reduzieren — Zahlen raus. + + ⚠ `lot_grund` nennt die freie Margin, und die aendert sich im Sekundentakt. + Ohne diese Normalisierung ist jede Drossel, die den TEXT vergleicht, + wirkungslos. Aus + "[MINDESTLOT] Einsatz 8 % von 39.33 EUR freier Margin = 3.15 EUR, ..." + wird "[MINDESTLOT] Einsatz # % von #", und das bleibt stabil. + """ + return re.sub(r"[0-9]+(?:[.,][0-9]+)?", "#", msg or "")[:60] + + class TradingEngine: """Headless-Pendant zu OilWidget: hält die core-Objekte, fährt die Refresh-Loops als Threads und liefert einen aggregierten Snapshot.""" @@ -1738,10 +1752,19 @@ class TradingEngine: füllt eine, existiert eine Position → `soll` wird False und die Gegenseite wird im selben Tick storniert. Das ersetzt ein echtes OCO, das MT5 nicht hat. """ - if not self._squeeze_pending: - return sym = self.data.symbol or "" - if not sym: + # ⚠⚠ AUFRAEUMEN GEHT VOR (User-Vorgabe 2026-08-20: "loesche die, wenn + # der Trade nicht eroeffnet werden kann"). Die beiden frueheren + # Fruehausstiege liessen liegende Orders ZURUECK: + # · `squeeze_pending_entry=false` waehrend Orders liegen + # · kein Symbol (MT5-Aussetzer) + # Sie sind `ORDER_TIME_GTC` — sie verfallen NICHT. Eine so verwaiste + # Order haengt beim Broker, wird von niemandem mehr bewertet und kann + # Stunden spaeter fuellen: ohne Dedup, ohne Guard-Pruefung und (bis zum + # Fix von heute) ohne Trailing. Deshalb wird jetzt in beiden Faellen + # ABGERAEUMT statt nur zurueckgekehrt. + if not self._squeeze_pending or not sym: + self._pendings_abraeumen(sym, "Feature aus" if sym else "kein Symbol") return try: # ⚠ NUR der eigene Slot entscheidet — vorher stornierte JEDE @@ -1782,6 +1805,13 @@ class TradingEngine: else: self.trader.cancel_pending(o.ticket) self._pending_tickets.pop(int(o.ticket), None) + # ⚠⚠ LOT-FEHLER = das Setup ist NICHT handelbar (User-Vorgabe + # 2026-08-20). Er heisst: die freie Margin reicht nicht einmal + # fuer das Mindestlot. Steht dann noch die GEGENSEITE aus einem + # frueheren Tick, laege ein halbes Setup am Markt — der Bot + # handelte den Ausbruch nur noch in EINE Richtung, waehrend die + # Messung beide unterstellt. Das wird unten aufgeloest. + lot_fehler = False for otype, preis in ziel.items(): if otype in vorhanden: continue @@ -1800,7 +1830,15 @@ class TradingEngine: 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})") - elif err and self._squeeze_pending_err.get(otype) != err: + # ⚠⚠ GEDROSSELT UEBER EINEN SCHLUESSEL, NICHT UEBER DEN TEXT + # (zweiter Fix am selben Tag). Heute Vormittag lag der Merker + # in EINER Variable fuer beide Seiten — behoben. Dann bekam + # die Meldung mit `lot_grund` ihre Zahlen, und die freie Margin + # aendert sich mit JEDEM Tick: der Text war jedes Mal neu, die + # Drossel damit erneut wirkungslos (2 Zeilen/Sekunde). + # Gedrosselt wird jetzt auf die URSACHE, nicht auf ihre + # Formulierung: alles bis zur ersten Zahl. + elif err and self._squeeze_pending_err.get(otype) != _err_key(err): # ⚠ Nur bei ÄNDERUNG loggen: „Kurs hat das Level bereits # passiert" tritt im 1-s-Takt auf, sobald der Kurs am Level # klebt. @@ -1812,14 +1850,49 @@ class TradingEngine: # Real am 20.08.: 269 Zeilen an einem Tag, 2 je Sekunde. # Eine Drossel, die nie drosselt, ist schlimmer als keine: # sie verdeckt im Log genau die Meldung, die zaehlt. - self._squeeze_pending_err[otype] = err + self._squeeze_pending_err[otype] = _err_key(err) log.info(f"Pending-Entry ({quelle}) nicht platzierbar " f"({preis:.3f}): {err}") + if not t and err and LOT_ZU_KLEIN in err: + lot_fehler = True + # ⚠ Nur beim LOT-Fehler abraeumen — NICHT bei "Kurs hat das Level + # bereits passiert". Der zweite ist voruebergehend und betrifft + # naturgemaess nur die eine Seite (der Kurs steht gerade an der + # Box-Kante); die Gegenseite bleibt ein gueltiger Einstieg. Der + # Lot-Fehler dagegen betrifft das KONTO und damit beide Seiten. + if lot_fehler and vorhanden: + self._pendings_abraeumen(sym, "Einsatz reicht nicht fuer das " + "Mindestlot — halbes Setup aufgeloest") self._pending_levels = ziel self._pending_quelle = quelle except Exception as e: log.error(f"_manage_squeeze_pending: {e}", exc_info=True) + def _pendings_abraeumen(self, sym: str, grund: str) -> int: + """Alle EIGENEN liegenden Orders stornieren. Rueckgabe: wie viele. + + ⚠ Fail-safe und still, wenn nichts liegt — die Methode laeuft im + 1-s-Takt. Geloggt wird nur, wenn wirklich storniert wurde. + """ + try: + if not sym: + sym = self.data.symbol or "" + if not sym: + return 0 + weg = 0 + for o in (self.trader.pending_orders(sym) or ()): + if not self.trader.cancel_pending(o.ticket): + self._pending_tickets.pop(int(o.ticket), None) + weg += 1 + if weg: + self._pending_levels = None + self._pending_quelle = "" + log.info(f"⏸ {weg} liegende Order(s) storniert — {grund}") + return weg + except Exception as e: + log.debug(f"_pendings_abraeumen: {e}") + return 0 + def _check_auto_squeeze(self): """Autonomer Entry auf den gemessen-validierten Volatilitäts-Squeeze-Ausbruch (`wave.squeeze.state == 'active'`), NUR wenn flat. Feuert **1× je Ausbruch** diff --git a/core/mt5_utils.py b/core/mt5_utils.py index e638e0b..ff0b5fc 100644 --- a/core/mt5_utils.py +++ b/core/mt5_utils.py @@ -117,6 +117,15 @@ def calc_lots(sym: str, price: float, otype: int, return min(lots, si.volume_max) +# ⚠⚠ MASCHINENLESBARER Marker fuer "Einsatz reicht nicht fuer das Mindestlot". +# Der Aufrufer (`_manage_squeeze_pending`) raeumt daraufhin das halbe Setup ab. +# Bewusst eine KONSTANTE und kein Wort im Fliesstext: eine Erkennung, die an +# einer deutschen Formulierung haengt, hoert still auf zu funktionieren, sobald +# jemand den Satz umschreibt — und faellt dabei auf die stille Seite (kein +# Abraeumen), nicht auf die laute. +LOT_ZU_KLEIN = "MINDESTLOT" + + def lot_grund(sym: str, price: float, otype: int, buffer_pct: float | None = None) -> str: """Warum liefert `calc_lots` 0? — als lesbarer Satz statt „Lot-Fehler". @@ -137,14 +146,14 @@ def lot_grund(sym: str, price: float, otype: int, return "Lot-Fehler (Margin je Lot nicht berechenbar)" _man = get_manual_margin() if _man > 0: - return (f"Einsatz {_man:.2f} EUR reicht nicht fuer das Mindestlot " - f"{si.volume_min} (braucht {si.volume_min * mpl:.2f} EUR)") + return (f"[{LOT_ZU_KLEIN}] Einsatz {_man:.2f} EUR reicht nicht fuer das " + f"Mindestlot {si.volume_min} (braucht {si.volume_min * mpl:.2f} EUR)") buf = (max(0.01, min(0.99, buffer_pct / 100.0)) if buffer_pct and buffer_pct > 0 else get_margin_buffer()) verf = acc.margin_free * buf - return (f"Einsatz {buf * 100:.0f} % von {acc.margin_free:.2f} EUR freier " - f"Margin = {verf:.2f} EUR, Mindestlot {si.volume_min} braucht aber " - f"{si.volume_min * mpl:.2f} EUR") + return (f"[{LOT_ZU_KLEIN}] Einsatz {buf * 100:.0f} % von {acc.margin_free:.2f} " + f"EUR freier Margin = {verf:.2f} EUR, Mindestlot {si.volume_min} " + f"braucht aber {si.volume_min * mpl:.2f} EUR") def calc_lots_risk(sym: str, price: float, otype: int,