"""Command-line-Schnittstelle (click).""" from __future__ import annotations import json import logging import sys import click from . import __version__ from .config import Config, load as load_config from .engine import save_state from .exchange import MockBroker, build_exchange from .strategy import Strategy from .trader import Trader from .trainer import Trainer, apply_params log = logging.getLogger("trademind.cli") def _setup_logging(verbose: bool) -> None: logging.basicConfig( level=logging.DEBUG if verbose else logging.INFO, format="%(asctime)s %(levelname)-7s %(name)s %(message)s", stream=sys.stderr, ) def get_strategy(cfg: Config, weights_path: str | None = None) -> Strategy: import os st = Strategy(cfg.strategy) if weights_path and os.path.exists(weights_path): with open(weights_path, "r", encoding="utf-8") as fh: wp = json.load(fh) st.weights = {**st.weights, **{k: float(v) for k, v in wp.get("weights", {}).items()}} return st @click.group() @click.version_option(__version__) def cli() -> None: """TradeMind – Krypto-Tradingbot (Paper/Sim + Live + Training).""" @cli.command("paper") @click.option("--config", "cfg_path", default="config.yaml", show_default=True) @click.option("--symbol", default=None, help="zB. BTC/USDT") @click.option("--candles", type=int, default=None) @click.option("--data", type=click.Choice(["auto", "live", "mock"]), default="auto", show_default=True, help="Kursdatenquelle: echte (live) oder generierte (mock)") @click.option("--exchange", default=None, help="Exchange für Kursdaten (zB. binance, kraken)") @click.option("--seed", type=int, default=None, help="Seed für deterministische Simulationsdaten") @click.option("--state", default="state/paper.json") def paper(cfg_path, symbol, candles, data, exchange, seed, state) -> None: """Paper-/Simulationslauf: nutzt ECHTE Marktkurse (Standard), Orders bleiben simuliert. Falls keine Kursdaten abrufbar sind (Offline), wird automatisch auf generierte mock-Daten zurückgefallen. """ cfg = load_config(cfg_path) if symbol: base, _, quote = symbol.partition("/") cfg.trading.base_currency = base cfg.trading.quote_currency = quote or "USDT" if candles: cfg.trading.candles = candles broker = _data_broker(cfg, data, exchange, seed) log.info("Paper-Modus: Kursdaten-Quelle = %s", broker.name) strat = get_strategy(cfg, cfg.training.state_file) trader = Trader(cfg, broker, strat) res = trader.simulate() save_state(state, res, strat.parameters()) click.echo(f"\n=== Paper-/Simulationslauf (Daten: {broker.name}) ===") click.echo(json.dumps(res.summary(), indent=2)) def _data_broker(cfg: Config, data: str, exchange: str | None, seed: int | None): """Live-Marktdaten via ccxt (ohne Keys). Fallback auf MockBroker.""" from .exchange import build_data_broker ex_name = exchange or (cfg.active_exchange().name if cfg.active_exchange() else "binance") if data != "mock": try: broker = build_data_broker(ex_name) broker.fetch_ticker(f"{cfg.trading.base_currency}/{cfg.trading.quote_currency}") return broker except Exception as e: # offline, Exchange down etc. if data == "live": raise SystemExit(f"Fehler beim Abruf der Live-Daten: {e}") click.echo(f"Warnung: Live-Daten nicht erreichbar ({e}). Fallback: mock-Daten.", err=True) return MockBroker(seed=seed if seed is not None else 7) @cli.command("live") @click.option("--config", "cfg_path", default="config.yaml", show_default=True) def live(cfg_path) -> None: """Ein Live-Zyklus: Bewertung + echte Order über die konfigurierte Exchange.""" cfg = load_config(cfg_path) ex = cfg.active_exchange() if not ex or not (ex.api_key and ex.api_secret): click.echo( "Keine Exchange mit API-Keys konfiguriert.\n" "Trage api_key/api_secret in config.yaml ein (oder über Env) und setze sandbox: false.", err=True, ) sys.exit(2) try: broker = build_exchange(ex) except Exception as e: # pragma: no cover click.echo(f"Exchange-Init fehlgeschlagen: {e}", err=True) sys.exit(1) strat = get_strategy(cfg, cfg.training.state_file) trader = Trader(cfg, broker, strat) order = trader.live_cycle() if order: click.echo("Order: " + json.dumps(order, indent=2)) else: click.echo("Kein Handels-Signal (HOLD).") @cli.command("train") @click.option("--config", "cfg_path", default="config.yaml", show_default=True) @click.option("--generations", type=int, default=None) @click.option("--population", type=int, default=None) @click.option("--data", type=click.Choice(["auto", "live", "mock"]), default="auto", show_default=True, help="Trainingsdatenquelle: echte (live) oder generierte (mock)") @click.option("--exchange", default=None, help="Exchange für Trainingsdaten (zB. binance, kraken)") def train(cfg_path, generations, population, data, exchange) -> None: """Trainiert die Strategie-Parameter auf Kursdaten (Standard: echte Marktkurse).""" cfg = load_config(cfg_path) if generations: cfg.training.generations = generations if population: cfg.training.population = population broker = _data_broker(cfg, data, exchange, 42) base_strat = get_strategy(cfg, cfg.training.state_file) symbol = f"{cfg.trading.base_currency}/{cfg.trading.quote_currency}" candles = broker.fetch_ohlcv(symbol, cfg.trading.timeframe, max(cfg.trading.candles, 500)) log.info("Trainingsdaten: %s (%d Candles)", broker.name, len(candles)) def progress(gen: int, best: float) -> None: click.echo(f"Generation {gen:>3}: beste Fitness = {best:.4f}") trainer = Trainer(cfg.training, base_strat) params = trainer.train(candles, cfg.trading, base_strat, progress=progress) apply_params(cfg.strategy, params) import os os.makedirs(os.path.dirname(cfg.training.state_file) or ".", exist_ok=True) with open(cfg.training.state_file, "w", encoding="utf-8") as fh: json.dump( { "params": params, "weights": { "ema_cross": params["w_ema_cross"], "rsi_long": params["w_rsi_long"], "rsi_exit": params["w_rsi_exit"], }, }, fh, indent=2, ) click.echo("\n=== Training abgeschlossen ===") click.echo("Optimierte Parameter: " + json.dumps(params, indent=2)) click.echo(f"Gewichte gespeichert in: {cfg.training.state_file}") @cli.command("show") @click.argument("path", default="config.yaml") def show(path) -> None: """Gibt die geladene Konfiguration aus (API-Keys werden maskiert).""" cfg = load_config(path) out = { "trading": cfg.trading.__dict__, "strategy": cfg.strategy.__dict__, "training": cfg.training.__dict__, "exchanges": { n: { "api_key": "***" if e.api_key else "", "api_secret": "***" if e.api_secret else "", "sandbox": e.sandbox, } for n, e in cfg.exchanges.items() }, } click.echo(json.dumps(out, indent=2)) def main() -> None: cli() if __name__ == "__main__": main()