Files
Trademind/src/trademind/notify.py
T
Tobias Zimmermann 65ed73977e Initial commit: TradeMind – Krypto-Trading-Bot mit Lernmodus
Per Podman deploybarer Bot, der Käufe und Verkäufe simuliert ausführt und sich
aus den Ergebnissen weiter antrainiert.

Aufbau
- Einheitliche Bar-Verarbeitung für paper, backtest und live; ausgetauscht werden
  nur Datenquelle und Broker.
- Börsenanbindung über ccxt: rund 100 Börsen allein über exchange.id erreichbar.
  Zugangsdaten kommen über ENV-Platzhalter, der Live-Modus ist doppelt abgesichert.
- Paper-Broker mit Gebühren, Slippage, Börsenpräzision und Volumengrenzen.
- Online trainierte logistische Regression bewertet jedes Einstiegssignal. Sie lernt
  aus realen Trade-Ergebnissen, aus Shadow-Labels aller Kandidaten – auch der
  abgelehnten – und aus Hintergrund-Stichproben; beim Kaltstart wird sie aus der
  Kurshistorie vorgelernt.
- Risikomanagement: Positions- und Exposure-Grenzen, ATR-Stops, Cooldown sowie
  Tagesverlust- und Drawdown-Notbremsen.
- SQLite-Persistenz, HTTP-Status mit Prometheus-Metriken und Dashboard, Webhooks.

Deployment
- Containerfile (zweistufig, non-root UID 10001), podman-compose, systemd-Quadlet.
- Modell und Datenbank liegen im Volume /data und überleben Neustarts.

128 Tests, ruff sauber. Verifiziert gegen echte Marktdaten sowie im gebauten
Container inklusive Healthcheck und Zustandswiederherstellung.
2026-08-22 08:53:04 +02:00

101 lines
3.8 KiB
Python
Raw 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.
"""Optionale Benachrichtigungen über einen generischen Webhook (Slack/Discord-kompatibel)."""
from __future__ import annotations
import asyncio
import logging
import aiohttp
from .config import NotificationConfig
from .models import Trade
log = logging.getLogger(__name__)
class Notifier:
"""Verschickt kurze Statusmeldungen. Fehler werden geloggt, nie weitergereicht."""
def __init__(self, config: NotificationConfig, timeout: float = 8.0) -> None:
self.config = config
self._timeout = aiohttp.ClientTimeout(total=timeout)
self._session: aiohttp.ClientSession | None = None
self._tasks: set[asyncio.Task[None]] = set()
@property
def enabled(self) -> bool:
return bool(self.config.webhook_url)
async def start(self) -> None:
if self.enabled and self._session is None:
self._session = aiohttp.ClientSession(timeout=self._timeout)
async def close(self) -> None:
for task in list(self._tasks):
task.cancel()
if self._tasks:
await asyncio.gather(*self._tasks, return_exceptions=True)
if self._session is not None:
await self._session.close()
self._session = None
def send_soon(self, message: str) -> None:
"""Nachricht im Hintergrund verschicken, ohne den Handels-Loop zu blockieren."""
if not self.enabled:
return
task = asyncio.create_task(self._send(message))
self._tasks.add(task)
task.add_done_callback(self._tasks.discard)
async def _send(self, message: str) -> None:
if self._session is None:
await self.start()
if self._session is None or not self.config.webhook_url:
return
# "text" bedient Slack, "content" bedient Discord ein Payload für beide.
payload = {"text": message, "content": message}
try:
async with self._session.post(self.config.webhook_url, json=payload) as response:
if response.status >= 400:
body = (await response.text())[:200]
log.warning("Webhook antwortete mit HTTP %s: %s", response.status, body)
except asyncio.CancelledError:
raise
except Exception as exc: # noqa: BLE001 - Benachrichtigungen dürfen nie den Bot stoppen
log.warning("Webhook-Zustellung fehlgeschlagen: %s", exc)
# ------------------------------------------------------------- Bausteine
def trade_closed(self, trade: Trade, equity: float, quote: str) -> None:
if not self.config.notify_on_trade:
return
icon = "🟢" if trade.is_win else "🔴"
tag = " [Exploration]" if trade.exploratory else ""
self.send_soon(
f"{icon} {trade.symbol} geschlossen ({trade.exit_reason.value}){tag}\n"
f"P/L: {trade.pnl_quote:+.2f} {quote} ({trade.pnl_pct * 100:+.2f}%) | "
f"Einstieg {trade.entry_price:.6f} → Ausstieg {trade.exit_price:.6f} | "
f"Equity: {equity:.2f} {quote}"
)
def position_opened(
self, symbol: str, amount: float, price: float, confidence: float, quote: str
) -> None:
if not self.config.notify_on_trade:
return
self.send_soon(
f"📈 Position eröffnet: {symbol} {amount:.8f} @ {price:.6f} "
f"(≈{amount * price:.2f} {quote}, Modellkonfidenz {confidence:.2f})"
)
def risk_halt(self, reason: str) -> None:
if not self.config.notify_on_risk_halt:
return
self.send_soon(f"⛔ Handel gestoppt: {reason}")
def startup(self, mode: str, exchange: str, symbols: list[str], timeframe: str) -> None:
self.send_soon(
f"🤖 TradeMind gestartet Modus **{mode}**, Börse {exchange}, "
f"Symbole {', '.join(symbols)} ({timeframe})"
)