"""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)