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.
207 lines
7.6 KiB
Python
207 lines
7.6 KiB
Python
"""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)
|