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.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
"""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})"
|
||||
)
|
||||
Reference in New Issue
Block a user