Files

220 lines
8.3 KiB
Python
Raw Permalink 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.
"""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("serve")
@click.option("--config", "cfg_path", default="config.yaml", show_default=True)
@click.option("--host", default=None, help="Bind-Adresse (Default: server.host aus config.yaml)")
@click.option("--port", type=int, default=None, help="Port (Default: server.port aus config.yaml)")
@click.option("--data", type=click.Choice(["auto", "live", "mock"]), default="auto",
show_default=True, help="Kursdatenquelle für die API")
@click.option("--exchange", default=None, help="Exchange für Kursdaten (zB. binance, kraken)")
def serve(cfg_path, host, port, data, exchange) -> None:
"""Startet die REST-API + HTML-Dashboard (Paper-Befehle, keine Live-Orders)."""
import os
cfg = load_config(cfg_path)
host = host or os.getenv("TM_HOST") or cfg.server.host
port = int(port or os.getenv("TM_PORT") or cfg.server.port)
from .server import run_server
run_server(cfg_path, host=host, port=port, data=data, exchange=exchange)
@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__,
"server": cfg.server.__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()