Ruhende Orders: aufraeumen statt liegenlassen (User-Vorgabe)

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 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-20 20:29:26 +02:00
co-authored by Claude Opus 5
parent 3fb9c3fd2e
commit fd2ca46dc0
3 changed files with 143 additions and 10 deletions
+51
View File
@@ -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, **Pipeline jetzt: A · A2 · A3 · B · B2 · C · C2 · D · E · F · G** — alle grün,
97 Tests, deployt und verifiziert. 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) ## ⚠⚠⚠ 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 Die fällige Messung ist entscheidbar geworden: **n=530 entkoppelt** gegen die
+78 -5
View File
@@ -13,6 +13,7 @@ das MT5-Terminal. Es sollte immer nur EINE Engine-Instanz gegen das Terminal
laufen. laufen.
""" """
from __future__ import annotations from __future__ import annotations
import re
import threading import threading
import time import time
from datetime import datetime from datetime import datetime
@@ -30,6 +31,7 @@ from core.config import (
# und die `[zones]`-Sektion in der ini erhalten — reaktivierbar, falls je gewollt. # und die `[zones]`-Sektion in der ini erhalten — reaktivierbar, falls je gewollt.
from core.logger import get_logger from core.logger import get_logger
from core.history import HistoryLogger from core.history import HistoryLogger
from core.mt5_utils import LOT_ZU_KLEIN
from core.mt5data import MT5Data from core.mt5data import MT5Data
from core.trader import TradeManager from core.trader import TradeManager
from core.trailing import TrailingManager from core.trailing import TrailingManager
@@ -202,6 +204,18 @@ def send_telegram(text, *a, **kw):
return _send_telegram_roh(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: class TradingEngine:
"""Headless-Pendant zu OilWidget: hält die core-Objekte, fährt die """Headless-Pendant zu OilWidget: hält die core-Objekte, fährt die
Refresh-Loops als Threads und liefert einen aggregierten Snapshot.""" 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 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. 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 "" 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 return
try: try:
# ⚠ NUR der eigene Slot entscheidet — vorher stornierte JEDE # ⚠ NUR der eigene Slot entscheidet — vorher stornierte JEDE
@@ -1782,6 +1805,13 @@ class TradingEngine:
else: else:
self.trader.cancel_pending(o.ticket) self.trader.cancel_pending(o.ticket)
self._pending_tickets.pop(int(o.ticket), None) 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(): for otype, preis in ziel.items():
if otype in vorhanden: if otype in vorhanden:
continue continue
@@ -1800,7 +1830,15 @@ class TradingEngine:
log.info(f"🎯 Pending-Entry ({quelle}) gesetzt: " log.info(f"🎯 Pending-Entry ({quelle}) gesetzt: "
f"{'BUY_STOP' if otype == mt5.ORDER_TYPE_BUY_STOP else 'SELL_STOP'}" f"{'BUY_STOP' if otype == mt5.ORDER_TYPE_BUY_STOP else 'SELL_STOP'}"
f" @ {preis:.3f} (T={t})") 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 # ⚠ Nur bei ÄNDERUNG loggen: „Kurs hat das Level bereits
# passiert" tritt im 1-s-Takt auf, sobald der Kurs am Level # passiert" tritt im 1-s-Takt auf, sobald der Kurs am Level
# klebt. # klebt.
@@ -1812,14 +1850,49 @@ class TradingEngine:
# Real am 20.08.: 269 Zeilen an einem Tag, 2 je Sekunde. # Real am 20.08.: 269 Zeilen an einem Tag, 2 je Sekunde.
# Eine Drossel, die nie drosselt, ist schlimmer als keine: # Eine Drossel, die nie drosselt, ist schlimmer als keine:
# sie verdeckt im Log genau die Meldung, die zaehlt. # 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 " log.info(f"Pending-Entry ({quelle}) nicht platzierbar "
f"({preis:.3f}): {err}") 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_levels = ziel
self._pending_quelle = quelle self._pending_quelle = quelle
except Exception as e: except Exception as e:
log.error(f"_manage_squeeze_pending: {e}", exc_info=True) 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): def _check_auto_squeeze(self):
"""Autonomer Entry auf den gemessen-validierten Volatilitäts-Squeeze-Ausbruch """Autonomer Entry auf den gemessen-validierten Volatilitäts-Squeeze-Ausbruch
(`wave.squeeze.state == 'active'`), NUR wenn flat. Feuert **1× je Ausbruch** (`wave.squeeze.state == 'active'`), NUR wenn flat. Feuert **1× je Ausbruch**
+14 -5
View File
@@ -117,6 +117,15 @@ def calc_lots(sym: str, price: float, otype: int,
return min(lots, si.volume_max) 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, def lot_grund(sym: str, price: float, otype: int,
buffer_pct: float | None = None) -> str: buffer_pct: float | None = None) -> str:
"""Warum liefert `calc_lots` 0? — als lesbarer Satz statt „Lot-Fehler". """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)" return "Lot-Fehler (Margin je Lot nicht berechenbar)"
_man = get_manual_margin() _man = get_manual_margin()
if _man > 0: if _man > 0:
return (f"Einsatz {_man:.2f} EUR reicht nicht fuer das Mindestlot " return (f"[{LOT_ZU_KLEIN}] Einsatz {_man:.2f} EUR reicht nicht fuer das "
f"{si.volume_min} (braucht {si.volume_min * mpl:.2f} EUR)") f"Mindestlot {si.volume_min} (braucht {si.volume_min * mpl:.2f} EUR)")
buf = (max(0.01, min(0.99, buffer_pct / 100.0)) buf = (max(0.01, min(0.99, buffer_pct / 100.0))
if buffer_pct and buffer_pct > 0 else get_margin_buffer()) if buffer_pct and buffer_pct > 0 else get_margin_buffer())
verf = acc.margin_free * buf verf = acc.margin_free * buf
return (f"Einsatz {buf * 100:.0f} % von {acc.margin_free:.2f} EUR freier " return (f"[{LOT_ZU_KLEIN}] Einsatz {buf * 100:.0f} % von {acc.margin_free:.2f} "
f"Margin = {verf:.2f} EUR, Mindestlot {si.volume_min} braucht aber " f"EUR freier Margin = {verf:.2f} EUR, Mindestlot {si.volume_min} "
f"{si.volume_min * mpl:.2f} EUR") f"braucht aber {si.volume_min * mpl:.2f} EUR")
def calc_lots_risk(sym: str, price: float, otype: int, def calc_lots_risk(sym: str, price: float, otype: int,