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
@@ -3139,6 +3139,51 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
|
|||||||
nächster Tick versucht erneut). Eigener `send_telegram`, **nicht** vom
|
nächster Tick versucht erneut). Eigener `send_telegram`, **nicht** vom
|
||||||
`pnl>0`-Gate (`history`) noch vom 5-Min-Cooldown betroffen.
|
`pnl>0`-Gate (`history`) noch vom 5-Min-Cooldown betroffen.
|
||||||
|
|
||||||
|
## ⚠⚠ TELEGRAM WAR NICHT KAPUTT — ES SENDETE NUR NICHTS MEHR (2026-08-11)
|
||||||
|
|
||||||
|
User: „ich bekomme keine Telegram-Nachrichten mehr."
|
||||||
|
✅ **Kanal nachweislich intakt:** `getMe` → `@AH_Oiltrader_bot`, `getChat` → privat
|
||||||
|
„Axel", **Testnachricht zugestellt** (message_id 1764). Im gesamten Log **keine
|
||||||
|
einzige** Fehlermeldung; der Tagesreport kam täglich (08.–11.08. je 07:3x, alle
|
||||||
|
`ok`). Letzte Nicht-Report-Nachricht **07.08. 22:49**, davor am 06.08. 19:33–19:58
|
||||||
|
im 5-Minuten-Takt = die Close-Alarm-Sperre.
|
||||||
|
⚠⚠ **DIE URSACHE: ALLE NEUN AUSLÖSER WAREN AUS** — Auto-Squeeze (über
|
||||||
|
`runtime_state.json`, unbemerkt), Auto-Signal, S/R-Auto-Close, Flip-Close,
|
||||||
|
15-Minuten-Regel, Circuit-Breaker und die **drei** Notfall-Stop-Modi. **Jede
|
||||||
|
Abschaltung war für sich begründet und gemessen; zusammen ergaben sie eine
|
||||||
|
Stille, die niemand beschlossen hatte.** Übrig blieb genau ein Live-Trigger, der
|
||||||
|
🔔 Close-Alarm — und der braucht eine **gerichtete** Empfehlung, die es nur in
|
||||||
|
**5 %** der Zeit gibt (letzte 7 Tage: 508 von 9.976 Zeilen).
|
||||||
|
⚠ Dazu kam: der Trade-Abschluss-Push war früher bewusst entfernt worden. Deshalb
|
||||||
|
kam auch der **−87-€-SL vom 10.08. wortlos**.
|
||||||
|
|
||||||
|
✅ **GEBAUT: Close-Benachrichtigung `engine._check_close_notify`**
|
||||||
|
(`[telegram] close_notify_min_eur = 50`, 0 = aus; Backup
|
||||||
|
`oil_widget_config.ini.bak-2026-08-11-closenotify`). Feuert bei **jedem**
|
||||||
|
geschlossenen Trade ab der Schwelle — **hängt an KEINER Automatik**, also auch
|
||||||
|
bei manuellem Close und Broker-SL. Meldet Betrag, Richtung, Lots, Ein-/Ausstieg,
|
||||||
|
**Schließgrund** und Dauer. Snapshot-Feld `close_notify_min`.
|
||||||
|
⚠ **Schwelle aus den Daten gewählt, nicht geraten:** bei ~17 Trades/Tag liefert
|
||||||
|
50 € rund **2,4–6,1 Meldungen/Tag** und erfasst **54–80 %** des bewegten Geldes.
|
||||||
|
⚠ **Eine absolute Euro-Schwelle altert mit der Positionsgröße** — 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.
|
||||||
|
⚠⚠ **Der Unterschied zum entfernten Push ist die SCHWELLE** (der alte feuerte bei
|
||||||
|
jedem Close), und sie liegt in der **Engine**, nicht in der DB-Schicht.
|
||||||
|
⚠ **Vorgemerkt statt sofort gesendet:** 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". Ohne diese
|
||||||
|
Unterscheidung meldete es einen Close **ohne P&L**. Aufgabe nach ~30 s.
|
||||||
|
✅ **7 Tests** (`tests/test_close_notify.py`, Netz abgefangen) + **Mutationsprobe
|
||||||
|
in beide kritischen Zweige**: Schwelle entfernt → `test_unter_schwelle` fällt ·
|
||||||
|
Retry-Fall als „kein Trade" behandelt → `test_db_noch_nicht_fertig` fällt.
|
||||||
|
|
||||||
|
✅ **AUTO-SQUEEZE WIEDER AN** (User-Entscheidung 2026-08-11) — über den offiziellen
|
||||||
|
Toggle `POST /api/autosqueeze`, damit ini UND `runtime_state.json` übereinstimmen
|
||||||
|
(`{"ok":true,"enabled":true}`). Der Config-Wächter meldet ihn seitdem nicht mehr.
|
||||||
|
⚠ Damit läuft die **B5-Abbruchregel** weiter (11/20 Trades) — ohne laufenden
|
||||||
|
Trader wäre sie nie abgelaufen. Latte unverändert: Verhältnis < 1,0 ODER PF < 1.
|
||||||
|
|
||||||
## Mobile-UI (web/) & Alarme
|
## Mobile-UI (web/) & Alarme
|
||||||
- Aufbau: **Tab-Navigation ganz oben** (sticky: Dashboard/**SIG Live**/**SIG Anzeige**/Logs/Statistik/News),
|
- Aufbau: **Tab-Navigation ganz oben** (sticky: Dashboard/**SIG Live**/**SIG Anzeige**/Logs/Statistik/News),
|
||||||
darunter Header.
|
darunter Header.
|
||||||
|
|||||||
@@ -186,6 +186,12 @@ class TradingEngine:
|
|||||||
token = tg.get("bot_token", "").strip(),
|
token = tg.get("bot_token", "").strip(),
|
||||||
chat_id = tg.get("chat_id", "").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 ────────────────────────────────────
|
# ── Markt / Trading / Trailing ────────────────────────────────────
|
||||||
self.data = MT5Data()
|
self.data = MT5Data()
|
||||||
@@ -393,6 +399,22 @@ class TradingEngine:
|
|||||||
self._open_pos_dir = None # Richtung der aktuell offenen Position (LONG/SHORT)
|
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._pos_close_ts = 0.0 # Zeit + Richtung des letzten Positions-Close (JEDE
|
||||||
self._news_block_logged = None # Dedup für die News-Blackout-Logzeile
|
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,4–6,1 Meldungen/Tag und erfasst 54–80 % 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
|
# Markt-zu-Erkennung über die letzte KURSBEWEGUNG (s. `snapshot`) — die
|
||||||
# Tick-Zeitstempel taugen dafür nicht (Abholzeit bzw. zirkulärer Offset).
|
# Tick-Zeitstempel taugen dafür nicht (Abholzeit bzw. zirkulärer Offset).
|
||||||
self._last_bid = None
|
self._last_bid = None
|
||||||
@@ -731,6 +753,9 @@ class TradingEngine:
|
|||||||
self.trader.refresh(self.data.symbol)
|
self.trader.refresh(self.data.symbol)
|
||||||
self._check_circuit_breaker() # Tagesverlust-Stopp (Überleben zuerst)
|
self._check_circuit_breaker() # Tagesverlust-Stopp (Überleben zuerst)
|
||||||
self._check_auto_close()
|
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_adverse15() # 15-Min-Regel (nur Bot-Trades)
|
||||||
self._check_flip_close() # Empfehlung dreht → schließen
|
self._check_flip_close() # Empfehlung dreht → schließen
|
||||||
self._check_sr_close()
|
self._check_sr_close()
|
||||||
@@ -873,6 +898,13 @@ class TradingEngine:
|
|||||||
if self._last_pos_ticket is not None and self._open_pos_dir:
|
if self._last_pos_ticket is not None and self._open_pos_dir:
|
||||||
self._pos_close_ts = time.time()
|
self._pos_close_ts = time.time()
|
||||||
self._pos_close_dir = self._open_pos_dir
|
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._open_pos_dir = None
|
||||||
self._last_pos_ticket = None
|
self._last_pos_ticket = None
|
||||||
self._emergency_loss = None
|
self._emergency_loss = None
|
||||||
@@ -4077,6 +4109,10 @@ class TradingEngine:
|
|||||||
"sr_close_min_gain": self._sr_close_min_gain,
|
"sr_close_min_gain": self._sr_close_min_gain,
|
||||||
"sr_close_pbreak": self._sr_close_pbreak,
|
"sr_close_pbreak": self._sr_close_pbreak,
|
||||||
"sr_close_count": self._sr_close_count,
|
"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,
|
"auto_squeeze": self._auto_squeeze,
|
||||||
# Nacht-Fenster mitliefern: nach `restart_server.bat` ist ein NEUES
|
# Nacht-Fenster mitliefern: nach `restart_server.bat` ist ein NEUES
|
||||||
# Snapshot-Feld der einzige verlässliche Beleg, dass wirklich der neue
|
# Snapshot-Feld der einzige verlässliche Beleg, dass wirklich der neue
|
||||||
@@ -4281,6 +4317,64 @@ class TradingEngine:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return None
|
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,
|
def _m15_setup(self, market, wave_snap, cone, alignment,
|
||||||
wave_signal=None) -> dict | None:
|
wave_signal=None) -> dict | None:
|
||||||
"""SETUP-BEREITSCHAFT auf M15 — der Ersatz für die entfernte Gesamtempfehlung.
|
"""SETUP-BEREITSCHAFT auf M15 — der Ersatz für die entfernte Gesamtempfehlung.
|
||||||
|
|||||||
@@ -407,6 +407,22 @@ class HistoryLogger:
|
|||||||
# alert „🔔 CLOSE-Signal") sowie bei Notfall-/Gewinn-Auto-Close. Die terse
|
# alert „🔔 CLOSE-Signal") sowie bei Notfall-/Gewinn-Auto-Close. Die terse
|
||||||
# „Sym +X"-Bestätigung bei jedem Close ist entfernt.
|
# „Sym +X"-Bestätigung bei jedem Close ist entfernt.
|
||||||
|
|
||||||
|
def trade_by_ticket(self, ticket: int) -> dict | None:
|
||||||
|
"""Einen Trade komplett lesen — für die Close-Benachrichtigung (2026-08-11).
|
||||||
|
|
||||||
|
⚠ Der Aufrufer muss `exit_time` prüfen: die Engine bemerkt den Flat-Zustand
|
||||||
|
über `trader.snapshot()` und kann dem DB-Schreiber um einen Tick voraus sein.
|
||||||
|
Ein Datensatz mit `exit_time IS NULL` heisst „noch nicht fertig", NICHT
|
||||||
|
„kein Trade" — wer das verwechselt, meldet einen Close ohne P&L.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with self._lock, self._connect() as conn:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT * FROM trades WHERE ticket = ?", (int(ticket),)).fetchone()
|
||||||
|
return dict(row) if row else None
|
||||||
|
except Exception:
|
||||||
|
return None # fail-open wie der Rest dieser Schicht
|
||||||
|
|
||||||
def log_ai(self, *, sentiment: str, confidence: int,
|
def log_ai(self, *, sentiment: str, confidence: int,
|
||||||
summary: str, drivers: list,
|
summary: str, drivers: list,
|
||||||
cost_estimate: float | None,
|
cost_estimate: float | None,
|
||||||
|
|||||||
@@ -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"]))
|
||||||
+2
-2
@@ -6,7 +6,7 @@
|
|||||||
<meta name="theme-color" content="#0d1117">
|
<meta name="theme-color" content="#0d1117">
|
||||||
<title>Oil · MT5</title>
|
<title>Oil · MT5</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
<link rel="stylesheet" href="style.css?v=160">
|
<link rel="stylesheet" href="style.css?v=161">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Modul-Menü (ganz oben) -->
|
<!-- Modul-Menü (ganz oben) -->
|
||||||
@@ -354,6 +354,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="toast" class="toast hidden"></div>
|
<div id="toast" class="toast hidden"></div>
|
||||||
|
|
||||||
<script src="app.js?v=160"></script>
|
<script src="app.js?v=161"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user