Files
Trademind/src/trademind/app.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

207 lines
7.6 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.
"""Zusammenbau aller Komponenten aus der Konfiguration."""
from __future__ import annotations
import logging
import sys
from dataclasses import dataclass
from typing import Any
from .broker import Broker, LiveBroker, PaperBroker
from .config import Config, Mode
from .data import CcxtDataFeed, DataFeed
from .engine import TradingEngine
from .exchange import build_exchange, load_market_info
from .features import N_FEATURES
from .notify import Notifier
from .portfolio import Portfolio
from .risk import RiskManager
from .server import StatusServer
from .storage import NullStorage, Storage
from .strategy import Strategy, build_strategy
log = logging.getLogger(__name__)
def setup_logging(level: str = "INFO") -> None:
root = logging.getLogger()
if root.handlers:
root.setLevel(level)
return
# Umlaute und Symbole sollen auch auf Konsolen mit anderer Codepage lesbar bleiben.
for stream in (sys.stdout, sys.stderr):
reconfigure = getattr(stream, "reconfigure", None)
if reconfigure is not None:
try:
reconfigure(encoding="utf-8", errors="replace")
except (ValueError, OSError): # pragma: no cover - je nach Konsole
pass
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(
logging.Formatter("%(asctime)s %(levelname)-7s %(name)-22s %(message)s", "%Y-%m-%d %H:%M:%S")
)
root.addHandler(handler)
root.setLevel(level)
# ccxt und aiohttp sind im INFO-Level sehr gesprächig.
logging.getLogger("ccxt").setLevel(logging.WARNING)
logging.getLogger("aiohttp").setLevel(logging.WARNING)
@dataclass
class Runtime:
"""Alle Laufzeitobjekte eines Laufs, inklusive geordnetem Herunterfahren."""
config: Config
engine: TradingEngine
broker: Broker
feed: DataFeed
strategy: Strategy
portfolio: Portfolio
risk: RiskManager
storage: Storage | NullStorage
notifier: Notifier
server: StatusServer | None
exchange: Any | None
async def start_services(self) -> None:
await self.notifier.start()
if self.server is not None:
await self.server.start()
async def close(self) -> None:
if self.server is not None:
await self.server.close()
await self.notifier.close()
if self.exchange is not None:
await self.exchange.close()
else:
await self.broker.close()
await self.feed.close()
self.storage.close()
def _quote_currency(market_info: dict[str, dict[str, Any]], symbols: list[str], fallback: str) -> str:
quotes = {market_info.get(s, {}).get("quote") for s in symbols}
quotes.discard(None)
if len(quotes) > 1:
raise ValueError(
"Alle Symbole müssen dieselbe Quote-Währung haben (gefunden: "
+ ", ".join(sorted(str(q) for q in quotes))
+ "). Bitte market.symbols anpassen."
)
return str(next(iter(quotes))) if quotes else fallback
async def build_runtime(
config: Config,
*,
with_server: bool = True,
with_storage: bool = True,
load_model: bool = True,
seed: int | None = None,
) -> Runtime:
"""Erzeugt Börsenanbindung, Broker, Strategie, Engine und Nebendienste."""
storage: Storage | NullStorage = (
Storage(config.storage.database_path) if with_storage else NullStorage()
)
read_only = config.mode is not Mode.LIVE
exchange = build_exchange(config.exchange, read_only=read_only)
market_info = await load_market_info(exchange, config.market.symbols)
feed = CcxtDataFeed(exchange)
broker: Broker
if config.mode is Mode.LIVE:
quote = _quote_currency(market_info, config.market.symbols, "USDT")
broker = LiveBroker(exchange, quote_currency=quote, market_info=market_info)
starting_equity = 0.0 # wird beim Start aus dem echten Guthaben gesetzt
else:
quote = _quote_currency(market_info, config.market.symbols, config.paper.quote_currency)
if quote != config.paper.quote_currency:
log.info(
"Quote-Währung der Symbole ist %s paper.quote_currency (%s) wird überschrieben",
quote, config.paper.quote_currency,
)
paper_config = config.paper.model_copy(update={"quote_currency": quote})
broker = PaperBroker(paper_config, market_info=market_info)
starting_equity = paper_config.starting_balance
strategy = build_strategy(config.strategy, N_FEATURES, seed=seed, load_model=load_model)
learner = getattr(strategy, "learner", None)
if learner is not None and config.mode is Mode.LIVE and config.strategy.learner.freeze_in_live:
learner.frozen = True
log.info("Live-Modus: Online-Lernen eingefroren (freeze_in_live=true)")
portfolio = Portfolio(starting_equity=starting_equity, quote_currency=broker.quote_currency)
risk = RiskManager(config.risk)
notifier = Notifier(config.notifications)
engine = TradingEngine(
config=config,
broker=broker,
feed=feed,
strategy=strategy,
portfolio=portfolio,
risk=risk,
storage=storage,
notifier=notifier,
)
server = StatusServer(config.server, engine.status) if (with_server and config.server.enabled) else None
storage.start_run(
mode=config.mode.value,
exchange=config.exchange.id,
symbols=config.market.symbols,
timeframe=config.market.timeframe,
strategy=config.strategy.name,
)
return Runtime(
config=config,
engine=engine,
broker=broker,
feed=feed,
strategy=strategy,
portfolio=portfolio,
risk=risk,
storage=storage,
notifier=notifier,
server=server,
exchange=exchange,
)
def describe_config(config: Config) -> str:
"""Kompakte Übersicht der wirksamen Konfiguration (ohne Geheimnisse)."""
ex = config.exchange
lines = [
f"Modus {config.mode.value}"
+ (" ⚠ ECHTES GELD" if config.mode is Mode.LIVE else ""),
f"Börse {ex.id}" + (" (Sandbox/Testnet)" if ex.sandbox else " (Produktiv)"),
f"Zugangsdaten {'gesetzt' if ex.has_credentials() else 'nicht gesetzt'}",
f"Symbole {', '.join(config.market.symbols)}",
f"Timeframe {config.market.timeframe} "
f"(Abfrage alle {config.market.poll_interval_seconds:g}s)",
f"Strategie {config.strategy.name}"
+ (f" (Lernen aktiv, Schwelle {config.strategy.learner.entry_threshold})"
if config.strategy.name == "adaptive" and config.strategy.learner.enabled
else " (kein Lernen)"),
f"Risiko max. {config.risk.max_open_positions} Positionen, "
f"{config.risk.max_position_pct:.0%} je Position, "
f"Stop {config.risk.stop_loss_atr_mult}×ATR, Ziel {config.risk.take_profit_atr_mult}×ATR",
f"Notbremsen Tagesverlust {config.risk.max_daily_loss_pct:.0%}, "
f"Drawdown {config.risk.max_drawdown_pct:.0%}",
f"Datenbank {config.storage.database_path}",
f"Modelldatei {config.strategy.learner.model_path}",
]
if config.mode is not Mode.LIVE:
lines.insert(
3,
f"Startkapital {config.paper.starting_balance:g} {config.paper.quote_currency} "
f"(Gebühr {config.paper.fee_rate:.3%}, Slippage {config.paper.slippage_bps:g} bps)",
)
if config.server.enabled:
lines.append(f"Status-Server http://{config.server.host}:{config.server.port}/")
return "\n".join(" " + line for line in lines)