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:
co-authored by
Claude Opus 5
parent
35e91474e8
commit
4eb92bf192
@@ -0,0 +1,121 @@
|
||||
"""Close-Benachrichtigung: feuert sie genau dann, wenn sie soll?
|
||||
|
||||
ANLASS (2026-08-11, User: „ich bekomme keine Telegram-Nachrichten mehr"). Der
|
||||
Kanal war intakt — eine Testnachricht wurde zugestellt —, aber ALLE NEUN
|
||||
sendenden Ausloeser waren aus. Jede Abschaltung war fuer sich begruendet;
|
||||
zusammen ergaben sie eine Stille, die niemand beschlossen hatte.
|
||||
|
||||
⚠ Der frueher entfernte Trade-Abschluss-Push scheiterte an fehlender SCHWELLE
|
||||
(jeder Close, ~17/Tag). Genau die wird hier geprueft — und der Renner-Fall, in
|
||||
dem die Engine dem DB-Schreiber einen Tick voraus ist.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
import core.engine as eng
|
||||
from core.engine import TradingEngine
|
||||
|
||||
|
||||
class _Hist:
|
||||
def __init__(self, trade):
|
||||
self.trade = trade
|
||||
self.aufrufe = 0
|
||||
|
||||
def trade_by_ticket(self, ticket):
|
||||
self.aufrufe += 1
|
||||
return self.trade
|
||||
|
||||
|
||||
class _Stub:
|
||||
"""Minimal-Engine — nur was `_check_close_notify` wirklich liest."""
|
||||
|
||||
_check_close_notify = TradingEngine._check_close_notify
|
||||
|
||||
def __init__(self, trade, min_eur=50.0, an=True):
|
||||
self.history = _Hist(trade)
|
||||
self._close_notify_min = min_eur
|
||||
self._close_notify_pending = [4711, 0]
|
||||
self.cfg = {"telegram": {"enabled": "true" if an else "false",
|
||||
"bot_token": "T", "chat_id": "C"}}
|
||||
|
||||
|
||||
def _trade(pnl=-87.03, fertig=True):
|
||||
return {"ticket": 4711, "symbol": "SpotCrude", "direction": "BUY",
|
||||
"lots": 1.77, "entry_price": 79.065, "exit_price": 78.497,
|
||||
"entry_time": 1000, "exit_time": 7180 if fertig else None,
|
||||
"pnl": pnl if fertig else None, "closed_by": "sl", "setup": "WAVE"}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _abfangen(monkeypatch):
|
||||
"""`send_telegram` abfangen — im Test geht NICHTS ins Netz."""
|
||||
gesendet = []
|
||||
monkeypatch.setattr(eng, "send_telegram",
|
||||
lambda msg, tok, cid: gesendet.append(msg))
|
||||
return gesendet
|
||||
|
||||
|
||||
def test_ueber_schwelle_wird_gesendet(_abfangen):
|
||||
"""Der reale −87,03-€-SL-Trade vom 10.08. — der kam bisher wortlos."""
|
||||
s = _Stub(_trade(-87.03))
|
||||
s._check_close_notify()
|
||||
assert len(_abfangen) == 1
|
||||
t = _abfangen[0]
|
||||
assert "-87.03" in t and "Broker-SL" in t and "🔴" in t
|
||||
assert s._close_notify_pending is None
|
||||
|
||||
|
||||
def test_unter_schwelle_bleibt_still(_abfangen):
|
||||
"""Genau der Grund, warum der alte Push als Spam entfernt wurde."""
|
||||
s = _Stub(_trade(12.40))
|
||||
s._check_close_notify()
|
||||
assert _abfangen == []
|
||||
assert s._close_notify_pending is None, "Vormerkung trotzdem aufraeumen"
|
||||
|
||||
|
||||
def test_gewinn_bekommt_gruenes_icon(_abfangen):
|
||||
s = _Stub(_trade(+229.41))
|
||||
s._check_close_notify()
|
||||
assert "🟢" in _abfangen[0] and "+229.41" in _abfangen[0]
|
||||
|
||||
|
||||
def test_db_noch_nicht_fertig_wird_erneut_versucht(_abfangen):
|
||||
"""⚠ Der gefaehrlichste Fall: der Flat-Zustand kommt aus `trader.snapshot()`
|
||||
und ist dem DB-Schreiber gelegentlich einen Tick voraus. `exit_time IS NULL`
|
||||
heisst „noch nicht fertig", NICHT „kein Trade" — wer das verwechselt, meldet
|
||||
einen Close ohne P&L."""
|
||||
s = _Stub(_trade(fertig=False))
|
||||
s._check_close_notify()
|
||||
assert _abfangen == []
|
||||
assert s._close_notify_pending == [4711, 1], "muss vorgemerkt BLEIBEN"
|
||||
# naechster Tick: DB ist durch
|
||||
s.history.trade = _trade(-87.03)
|
||||
s._check_close_notify()
|
||||
assert len(_abfangen) == 1 and s._close_notify_pending is None
|
||||
|
||||
|
||||
def test_gibt_nach_30_versuchen_auf(_abfangen):
|
||||
"""Ein Fremd-Trade ohne DB-Zeile darf nicht ewig gepollt werden."""
|
||||
s = _Stub(None)
|
||||
for _ in range(31):
|
||||
s._check_close_notify()
|
||||
assert _abfangen == []
|
||||
assert s._close_notify_pending is None
|
||||
|
||||
|
||||
def test_telegram_aus_sendet_nicht(_abfangen):
|
||||
s = _Stub(_trade(-200.0), an=False)
|
||||
s._check_close_notify()
|
||||
assert _abfangen == []
|
||||
|
||||
|
||||
def test_ohne_vormerkung_passiert_nichts(_abfangen):
|
||||
s = _Stub(_trade(-200.0))
|
||||
s._close_notify_pending = None
|
||||
s._check_close_notify()
|
||||
assert _abfangen == [] and s.history.aufrufe == 0, "keine DB-Last ohne Anlass"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(pytest.main([__file__, "-v"]))
|
||||
Reference in New Issue
Block a user