From b448a7ade801711cebdcc7463c8b28d5dd176d2d Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Thu, 20 Aug 2026 14:26:43 +0200 Subject: [PATCH] Warum BRK heute nicht ausgeloest hat: drei Ursachen, zwei behoben User: "warum wurde beim letzten anstieg kein Break Out trade ausgeloest?" Der Squeeze HAT gefeuert - mit der echten `_squeeze_one` nachgerechnet: 8 Bars `active` heute, davon 6 LONG (00:55 / 03:00 / 07:40 / 08:35 / 11:50 / 13:00). `squeeze_entry_count` steht trotzdem auf 0. Im Log stehen 8 `AUTO-SQUEEZE-ENTRY`-Zeilen - die Order scheiterte JEDES MAL. URSACHE 1 - "Position bereits offen!" (1x, 08:30:34). Der Market-Fallback lief noch ueber Slot 1, der vom manuellen Trade belegt war. Genau die Zwei-Slot- Luecke; der `_slot()`-Fix kam um 08:32 und hat sie geschlossen. URSACHE 2 - "Lot-Fehler" (140x, ab 13:02 JEDER Versuch). Das ist der Hauptgrund und er ist eine Einstellung, kein Defekt: `margin_brk = 10 %`, und `calc_lots` nimmt einen Prozentsatz der FREIEN Margin, nicht des Kontos. Die manuelle Position bindet 707 von 776 EUR -> 10 % von ~100 EUR frei = ~10 EUR, ein Mindestlot braucht 7,47 EUR, und die freie Margin schwankt mit dem P&L. Damit lag BRK den ganzen Tag auf der Kippe. NICHT eigenmaechtig geaendert - das Feld ist eine User-Entscheidung. URSACHE 3 - "Kurs hat das Level bereits passiert" (129x). Die BUY_STOP-Seite war nicht platzierbar, weil das Box-Dach dem Kurs ENTGEGENKAM (rollierendes Fenster) - die wandernde Marke aus backtest_squeeze_frozen.py, hier live. BEHOBEN (1): LOG-DROSSEL GRIFF NIE. Der Merker lag in EINER Variable, die beiden Seiten scheitern aber im selben Tick mit VERSCHIEDENEN Fehlern ("Level passiert" oben, "Lot-Fehler" unten) - der Wert wechselte bei jedem Aufruf. Real: 269 Zeilen an einem Tag, 2 je Sekunde. Jetzt je Order-Seite gemerkt. Eine Drossel, die nie drosselt, ist schlimmer als keine: sie verdeckt im Log genau die Meldung, die zaehlt. BEHOBEN (2): "Lot-Fehler" erklaert sich jetzt selbst (`mt5_utils.lot_grund`). Statt zwei Woertern steht dort "Einsatz 2 % von 101.90 EUR freier Margin = 2.04 EUR, Mindestlot 0.01 braucht aber 7.47 EUR" Beide Aufrufer (Market `_send` und Pending `place_stop`) nutzen ihn. Live gegengeprueft an drei Einsatz-Werten. Pipeline gruen (check_nfalle, Stufe E, 97 Tests), deployt und verifiziert: seit dem Neustart 0 Zeilen "nicht platzierbar". Co-Authored-By: Claude Opus 5 --- core/engine.py | 19 ++++++++++++++----- core/mt5_utils.py | 30 ++++++++++++++++++++++++++++++ core/trader.py | 10 ++++++---- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/core/engine.py b/core/engine.py index e314938..3e854d1 100644 --- a/core/engine.py +++ b/core/engine.py @@ -471,7 +471,7 @@ class TradingEngine: # 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_pending_err: dict = {} # je Order-Seite, s. unten self._squeeze_chase_logged = None self._squeeze_reverse_sig = None # Dedup Reverse-Close (Gegen-Trade) self._squeeze_entry_count = 0 @@ -1774,10 +1774,19 @@ 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 != err: - # ⚠ Nur bei ÄNDERUNG loggen: „Kurs hat das Level bereits passiert" - # tritt im 1-s-Takt auf, sobald der Kurs am Level klebt. - self._squeeze_pending_err = err + elif err and self._squeeze_pending_err.get(otype) != err: + # ⚠ Nur bei ÄNDERUNG loggen: „Kurs hat das Level bereits + # passiert" tritt im 1-s-Takt auf, sobald der Kurs am Level + # klebt. + # ⚠⚠ JE SEITE gemerkt (Fix 2026-08-20). Vorher lag der + # Merker in EINER Variable — und weil die beiden Seiten + # im selben Tick mit VERSCHIEDENEN Fehlern scheitern + # ("Level passiert" oben, "Lot-Fehler" unten), wechselte + # der Wert bei jedem Aufruf und die Sperre griff NIE. + # 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 log.info(f"Pending-Entry ({quelle}) nicht platzierbar " f"({preis:.3f}): {err}") self._pending_levels = ziel diff --git a/core/mt5_utils.py b/core/mt5_utils.py index 8d16fed..e638e0b 100644 --- a/core/mt5_utils.py +++ b/core/mt5_utils.py @@ -117,6 +117,36 @@ def calc_lots(sym: str, price: float, otype: int, return min(lots, si.volume_max) +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". + + ⚠⚠ ANLASS (2026-08-20, real): der Breakout-Trader hat den ganzen Tag keinen + Trade eroeffnet und meldete nur „Lot-Fehler". Die Ursache war ein + Zusammenspiel, das man der Meldung NICHT ansah: das Feld „Einsatz je Pfad" + stand auf 10 %, und weil `calc_lots` einen Prozentsatz der FREIEN Margin + nimmt — nicht des Kontos —, blieben von 108 EUR frei nur 10,83 EUR uebrig, + waehrend ein Mindestlot 7,47 EUR braucht und die freie Margin schwankt. + Eine Fehlermeldung, die den Grund verschweigt, kostet hier Stunden. + """ + si, acc = mt5.symbol_info(sym), mt5.account_info() + if not si or not acc: + return "Lot-Fehler (kein Symbol/Konto)" + mpl = mt5.order_calc_margin(otype, sym, 1.0, price) + if not mpl or mpl <= 0: + 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)") + 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") + + def calc_lots_risk(sym: str, price: float, otype: int, sl_distance: float | None, risk_frac: float) -> float: """Risiko-basierte Lot-Größe: Verlust beim Initial-SL ≈ risk_frac × Equity. diff --git a/core/trader.py b/core/trader.py index 4004a9e..be6b73c 100644 --- a/core/trader.py +++ b/core/trader.py @@ -19,8 +19,7 @@ from core.config import ( ) from core.mt5_utils import ( mt5_lock, get_tick, get_filling, calc_lots, calc_lots_risk, - pivot_low, pivot_high, atr_value, -) + pivot_low, pivot_high, atr_value, lot_grund,) from core.logger import get_logger # ⚠ NUR für die Typ-Annotation `'HistoryLogger | None'` (String, wird zur Laufzeit @@ -238,7 +237,8 @@ class TradeManager: else: lots = calc_lots(sym, price, otype, buffer_pct) # margin-basiert (risk_pct=0) if lots <= 0: - self.last_error = "Lot-Fehler"; return None, 0.0 + self.last_error = lot_grund(sym, price, otype, buffer_pct) + return None, 0.0 req = { "action": mt5.TRADE_ACTION_DEAL, "symbol": sym, "volume": float(lots), @@ -330,7 +330,9 @@ class TradeManager: mt5.ORDER_TYPE_BUY if ist_long else mt5.ORDER_TYPE_SELL, buffer_pct) if lots <= 0: - return None, "Lot-Fehler" + return None, lot_grund(sym, trigger, + mt5.ORDER_TYPE_BUY if ist_long + else mt5.ORDER_TYPE_SELL, buffer_pct) req = {"action": mt5.TRADE_ACTION_PENDING, "symbol": sym, "volume": float(lots), "type": otype, "price": float(trigger), "magic": MAGIC, "comment": comment,