Initial release: TradeMind crypto trading bot with paper/live modes and strategy training

This commit is contained in:
Tobias Zimmermann
2026-08-22 11:53:59 +02:00
commit 7959dd71ff
18 changed files with 1604 additions and 0 deletions
+199
View File
@@ -0,0 +1,199 @@
"""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()