220 lines
8.3 KiB
Python
220 lines
8.3 KiB
Python
"""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()
|