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,206 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user