65ed73977e
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.
101 lines
3.8 KiB
Python
101 lines
3.8 KiB
Python
"""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})"
|
||
)
|