Telegram: Close-Benachrichtigung ab Schwelle + Auto-Squeeze wieder an

Diagnose zu "ich bekomme keine Telegram-Nachrichten mehr": der Kanal
war intakt (getMe/getChat ok, Testnachricht zugestellt, keine einzige
Fehlermeldung im Log, Tagesreport kam taeglich). Ursache war, dass
ALLE NEUN sendenden Ausloeser aus waren - Auto-Squeeze (ueber
runtime_state.json, unbemerkt), Auto-Signal, S/R-Close, Flip-Close,
15-Minuten-Regel, Circuit-Breaker und die drei Notfall-Stop-Modi.
Jede Abschaltung war fuer sich begruendet; zusammen ergaben sie eine
Stille, die niemand beschlossen hatte.

Gebaut: engine._check_close_notify - Telegram bei JEDEM geschlossenen
Trade ab close_notify_min_eur (50, 0 = aus). Haengt an keiner
Automatik, feuert also auch bei manuellem Close und Broker-SL. Der
-87-EUR-SL vom 10.08. kam bisher wortlos.

Schwelle aus den Daten: bei ~17 Trades/Tag rund 2,4-6,1 Meldungen/Tag,
erfasst 54-80 % des bewegten Geldes. Absolute Euro-Schwelle altert mit
der Positionsgroesse - bewusst so, ein Mensch denkt in Euro.

Der Unterschied zum frueher entfernten Push ist die Schwelle, und sie
liegt in der Engine statt in der DB-Schicht. Vorgemerkt statt sofort
gesendet, weil der Flat-Zustand dem DB-Schreiber einen Tick voraus
sein kann (exit_time IS NULL heisst "noch nicht fertig").

- tests/test_close_notify.py: 7 Tests, Netz abgefangen
- Mutationsprobe in beide kritischen Zweige, beide gefangen
- Snapshot-Feld close_notify_min (belegt die Schwelle im Prozess)
- Auto-Squeeze wieder AN ueber /api/autosqueeze (ini + runtime_state)
- v=161

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-11 09:22:41 +02:00
co-authored by Claude Opus 5
parent 35e91474e8
commit 4eb92bf192
5 changed files with 278 additions and 2 deletions
+94
View File
@@ -186,6 +186,12 @@ class TradingEngine:
token = tg.get("bot_token", "").strip(),
chat_id = tg.get("chat_id", "").strip(),
)
# Schwelle für die Close-Benachrichtigung (0 = aus). Siehe __init__.
try:
self._close_notify_min = max(
0.0, float(tg.get("close_notify_min_eur", "50") or 0))
except (TypeError, ValueError):
self._close_notify_min = 50.0
# ── Markt / Trading / Trailing ────────────────────────────────────
self.data = MT5Data()
@@ -393,6 +399,22 @@ class TradingEngine:
self._open_pos_dir = None # Richtung der aktuell offenen Position (LONG/SHORT)
self._pos_close_ts = 0.0 # Zeit + Richtung des letzten Positions-Close (JEDE
self._news_block_logged = None # Dedup für die News-Blackout-Logzeile
# ── Close-Benachrichtigung (2026-08-11, User: „ich bekomme keine Telegram-
# Nachrichten mehr"). Diagnose damals: der Kanal war intakt (Testnachricht
# zugestellt), aber ALLE NEUN Auslöser waren aus — jeder einzeln begründet,
# zusammen aber ein Nebeneffekt, den niemand beschlossen hatte. Diese
# Meldung hängt bewusst an KEINER Automatik: sie feuert bei jedem Close,
# auch bei manuellen und bei Broker-SL. Genau der 87-€-SL vom 10.08. kam
# sonst wortlos.
# ⚠ Schwelle aus den Daten gewählt, nicht geraten: bei ~17 Trades/Tag liefert
# 50 € rund 2,46,1 Meldungen/Tag und erfasst 5480 % des bewegten Geldes.
# ⚠ Eine ABSOLUTE Euro-Schwelle altert mit der Positionsgrösse (Median |P&L|
# über 30 Tage 14,65 €, über die letzten 7 Tage 35,80 €) — bei wachsendem
# Konto kommen mehr Meldungen. Bewusst so: ein Mensch denkt in Euro.
# ⚠ `_close_notify_min` wird WEITER OBEN aus der ini gelesen (bei der
# Telegram-Konfiguration) — hier NICHT erneut zuweisen, das ueberschriebe
# den Wert mit 0 und die Meldung waere still tot.
self._close_notify_pending = None # [ticket, Versuche] bis `exit_time` steht
# Markt-zu-Erkennung über die letzte KURSBEWEGUNG (s. `snapshot`) — die
# Tick-Zeitstempel taugen dafür nicht (Abholzeit bzw. zirkulärer Offset).
self._last_bid = None
@@ -731,6 +753,9 @@ class TradingEngine:
self.trader.refresh(self.data.symbol)
self._check_circuit_breaker() # Tagesverlust-Stopp (Überleben zuerst)
self._check_auto_close()
# ⚠ NACH `_check_auto_close`: erst dort wird der Flat-Übergang
# erkannt und die Benachrichtigung vorgemerkt.
self._check_close_notify() # Telegram bei jedem Close ab Schwelle
self._check_adverse15() # 15-Min-Regel (nur Bot-Trades)
self._check_flip_close() # Empfehlung dreht → schließen
self._check_sr_close()
@@ -873,6 +898,13 @@ class TradingEngine:
if self._last_pos_ticket is not None and self._open_pos_dir:
self._pos_close_ts = time.time()
self._pos_close_dir = self._open_pos_dir
# ⚠ Close-Benachrichtigung nur VORMERKEN, nicht hier senden: der
# Flat-Zustand kommt aus `trader.snapshot()` und kann dem
# DB-Schreiber um einen Tick voraus sein. `_check_close_notify`
# wartet, bis `exit_time` steht — sonst meldete es einen Close
# ohne P&L.
if self._close_notify_min > 0:
self._close_notify_pending = [self._last_pos_ticket, 0]
self._open_pos_dir = None
self._last_pos_ticket = None
self._emergency_loss = None
@@ -4077,6 +4109,10 @@ class TradingEngine:
"sr_close_min_gain": self._sr_close_min_gain,
"sr_close_pbreak": self._sr_close_pbreak,
"sr_close_count": self._sr_close_count,
# Schwelle der Close-Benachrichtigung — sichtbar, damit belegbar ist,
# dass sie im LAUFENDEN Prozess angekommen ist. „Erfolg melden, wo
# nichts geprueft wurde" ist im Projekt bereits viermal passiert.
"close_notify_min": self._close_notify_min,
"auto_squeeze": self._auto_squeeze,
# Nacht-Fenster mitliefern: nach `restart_server.bat` ist ein NEUES
# Snapshot-Feld der einzige verlässliche Beleg, dass wirklich der neue
@@ -4281,6 +4317,64 @@ class TradingEngine:
except Exception:
return None
def _check_close_notify(self):
"""Telegram bei jedem geschlossenen Trade ab `close_notify_min_eur`.
WARUM ES DAS GIBT (2026-08-11): der Kanal war nachweislich intakt
(Testnachricht zugestellt), aber ALLE NEUN sendenden Auslöser waren aus
Auto-Squeeze, Auto-Signal, S/R-Close, Flip-Close, 15-Minuten-Regel,
Circuit-Breaker und die drei Notfall-Stop-Modi. Jede Abschaltung war für
sich begründet; zusammen ergaben sie eine Stille, die niemand beschlossen
hatte. Diese Meldung hängt deshalb bewusst an **keiner** Automatik: sie
feuert auch bei manuellem Close und bei Broker-SL.
Der Trade-Abschluss-Push war früher in `history.log_trade_close` und wurde
als Spam entfernt (jeder Close, ohne Schwelle). Der Unterschied ist die
**Schwelle** und sie steckt hier in der Engine, nicht in der DB-Schicht.
"""
p = self._close_notify_pending
if not p:
return
ticket, versuche = p
tr = self.history.trade_by_ticket(ticket)
# ⚠ `exit_time IS NULL` heisst „DB-Schreiber noch nicht durch", nicht
# „kein Trade". Der Flat-Zustand kommt aus `trader.snapshot()` und ist
# dem Schreiber gelegentlich einen Tick voraus.
if not tr or tr.get("exit_time") is None or tr.get("pnl") is None:
p[1] = versuche + 1
if versuche >= 30: # ~30 s — dann war es kein Bot-Trade
self._close_notify_pending = None
return
self._close_notify_pending = None
pnl = float(tr["pnl"] or 0.0)
if abs(pnl) < self._close_notify_min:
return
tg = self.cfg["telegram"]
if tg.get("enabled", "false").strip().lower() != "true":
return
try:
dauer = ""
if tr.get("entry_time"):
mins = (int(tr["exit_time"]) - int(tr["entry_time"])) / 60.0
dauer = (f"{mins/60:.1f} h" if mins >= 90 else f"{mins:.0f} min")
grund = {"sl": "Broker-SL", "manual": "manuell",
"sr_close": "S/R-Auto-Close", "emergency": "Notfall-Stop",
"timestop": "Time-Stop", "adverse15": "15-Minuten-Regel",
"flip_close": "Signal-Flip",
"circuit_breaker": "Circuit Breaker",
"squeeze_reverse": "Squeeze-Reverse"}.get(
tr.get("closed_by"), tr.get("closed_by") or "?")
icon = "🟢" if pnl > 0 else "🔴"
send_telegram(
f"{icon} <b>Trade geschlossen</b> {pnl:+.2f}\n"
f"{tr.get('symbol') or ''} {tr.get('direction') or ''} "
f"{tr.get('lots')} Lots · {tr.get('entry_price')}"
f"{tr.get('exit_price')}\n"
f"{grund} · {dauer} · Setup {tr.get('setup') or ''}",
tg.get("bot_token", "").strip(), tg.get("chat_id", "").strip())
log.info(f"📨 Close-Push #{ticket} {pnl:+.2f} € ({grund})")
except Exception as e:
log.debug(f"close_notify: {e}")
def _m15_setup(self, market, wave_snap, cone, alignment,
wave_signal=None) -> dict | None:
"""SETUP-BEREITSCHAFT auf M15 — der Ersatz für die entfernte Gesamtempfehlung.