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:
Tobias Zimmermann
2026-08-22 08:53:04 +02:00
commit 65ed73977e
40 changed files with 7033 additions and 0 deletions
+100
View File
@@ -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})"
)