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,
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
+78 -5
View File
@@ -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**
+14 -5
View File
@@ -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,