Files
AH-Oil-Trader/tests/test_close_notify.py
Axel HocksandClaude Opus 5 4eb92bf192 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>
2026-08-11 09:22:41 +02:00

122 lines
4.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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"]))