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 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-20 14:26:43 +02:00
co-authored by Claude Opus 5
parent a35ea38d31
commit b448a7ade8
3 changed files with 50 additions and 9 deletions
+14 -5
View File
@@ -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
+30
View File
@@ -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.
+6 -4
View File
@@ -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,